刘帅
7 天以前 d41e0da5df20d559b0b9b8c180d1bf70ba7c1845
cloud-server-management/src/main/java/com/dsh/guns/modular/system/controller/system/UserMgrController.java
@@ -113,14 +113,21 @@
    private List<TStore> availableStores() {
        LambdaQueryWrapper<TStore> query = new LambdaQueryWrapper<TStore>().eq(TStore::getState, 1);
        Integer objectType = UserExt.getUser().getObjectType();
        Integer objectId = UserExt.getUser().getObjectId();
        if (Integer.valueOf(2).equals(objectType)) {
            query.eq(TStore::getOperatorId, objectId);
        } else if (Integer.valueOf(3).equals(objectType)) {
            query.eq(TStore::getId, objectId);
        User currentUser = UserExt.getUser();
        // 超级管理员可维护全部启用门店;其他员工仅能看到自己被分配的门店。
        // getStoreIdsByUserId 同时兼容 sys_user_store、多门店改造前的 objectId,及历史店长数据。
        if (!isSuperAdmin(currentUser)) {
            List<Integer> storeIds = userMapper.getStoreIdsByUserId(currentUser.getId());
            if (storeIds == null || storeIds.isEmpty()) {
                return Collections.emptyList();
            }
            query.in(TStore::getId, storeIds);
        }
        return storeService.list(query);
    }
    private boolean isSuperAdmin(User user) {
        return user != null && "admin".equalsIgnoreCase(user.getAccount());
    }
    /**
@@ -153,9 +160,16 @@
        User user = this.userService.getById(userId);
        Role role = roleService.getById(user.getRoleid());
        Dept dept = deptService.getById(user.getDeptid());
        // 兼容历史账号:部分账号的登录手机号仅保存在 account 字段。
        // 个人资料页必须能回显该绑定手机号,避免前端提交空值。
        if (StringUtils.isEmpty(user.getPhone()) && !StringUtils.isEmpty(user.getAccount())) {
            user.setPhone(user.getAccount());
        }
        user.setPhone(maskPhone(user.getPhone()));
        user.setIdCard(maskIdCard(user.getIdCard()));
        model.addAttribute(user);
        // 显式指定模型名称和身份证回显字段,避免模板按隐式参数名解析失败。
        model.addAttribute("user", user);
        model.addAttribute("idCard", user.getIdCard());
        if(role==null){
            model.addAttribute("roleName", "");
        }else {
@@ -214,6 +228,11 @@
        User user = this.userService.getById(userId);
        Role role = roleService.getById(user.getRoleid());
        Dept dept = deptService.getById(user.getDeptid());
        // 兼容历史账号:部分账号的登录手机号仅保存在 account 字段。
        // 个人资料页必须能回显该绑定手机号,避免前端提交空值。
        if (StringUtils.isEmpty(user.getPhone()) && !StringUtils.isEmpty(user.getAccount())) {
            user.setPhone(user.getAccount());
        }
        user.setPhone(maskPhone(user.getPhone()));
        user.setIdCard(maskIdCard(user.getIdCard()));
        model.addAttribute(user);
@@ -301,10 +320,14 @@
//                return res1;
                return filteredList;
            case 3:
                // 门店只查询门店员工
                TStore store = storeService.getById(objectId);
                List<Integer> storeIds = userQuery.getStoreIds();
                storeIds.add(store.getStoreStaffId());
                // 门店员工可同时归属多个门店,员工列表应汇总这些门店下的所有员工。
                List<Integer> currentStoreIds = userMapper.getStoreIdsByUserId(UserExt.getUser().getId());
                List<Integer> employeeIds = currentStoreIds == null || currentStoreIds.isEmpty()
                        ? Collections.<Integer>emptyList()
                        : userMapper.getUserIdsByStoreIds(currentStoreIds);
                // searchUsers 在集合为空时不会拼接 IN 条件;用不存在的 ID 明确返回空结果,避免越权看到全部员工。
                userQuery.setStoreIds(employeeIds == null || employeeIds.isEmpty()
                        ? Collections.singletonList(-1) : employeeIds);
                List<UserListVO> res2= userService.searchUsers(userQuery);
                return res2;
        }
@@ -381,19 +404,8 @@
        objectUser.setCreatetime(new Date());
        List<TStore> assignedStores = new ArrayList<TStore>();
        if (user.getStoreIds() != null && !user.getStoreIds().isEmpty()) {
            List<Integer> storeIds = user.getStoreIds().stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
            if (storeIds.isEmpty()) return ResultUtil.error("请选择有效门店");
            Integer currentType = UserExt.getUser().getObjectType();
            Integer currentObjectId = UserExt.getUser().getObjectId();
            for (Integer storeId : storeIds) {
                TStore store = storeService.getById(storeId);
                if (store == null || !Integer.valueOf(1).equals(store.getState())) return ResultUtil.error("所选门店不存在或已停用");
                if ((Integer.valueOf(2).equals(currentType) && !currentObjectId.equals(store.getOperatorId()))
                        || (Integer.valueOf(3).equals(currentType) && !currentObjectId.equals(store.getId()))) {
                    return ResultUtil.error("所选门店不在当前账号的管理范围内");
                }
                assignedStores.add(store);
            }
            assignedStores = validateStores(user.getStoreIds());
            if (assignedStores == null || assignedStores.isEmpty()) return ResultUtil.error("所选门店不存在、已停用或不在当前账号的管理范围内");
            // 首个门店同步写入旧字段,保证既有权限、数据查询和登录逻辑无需改造。
            TStore primaryStore = assignedStores.get(0);
            objectUser.setObjectType(3);
@@ -434,9 +446,12 @@
        }
        User oldUser = userService.getById(user.getId());
        // 判断是否更改电话
        if (user.getPhone() != null && user.getPhone().contains("*")) {
        if (StringUtils.isEmpty(user.getPhone()) || user.getPhone().contains("*")) {
            // The edit view displays a masked phone. Treat an unchanged masked
            // value as "keep original", never as a value to persist.
            // value, or an omitted field, as "keep original", never as a
            // value to persist. Some edit forms do not submit disabled or
            // hidden fields; allowing an empty value through would erase the
            // stored phone after deployment.
        } else if (!Objects.equals(oldUser.getPhone(), user.getPhone())) {
            User phone = userService.getOne(new QueryWrapper<User>().eq("phone", user.getPhone()));
            if (phone == null){
@@ -448,7 +463,11 @@
        oldUser.setName(user.getName());
        oldUser.setBirthday(user.getBirthday());
        oldUser.setDeptid(user.getDeptid());
        if (user.getIdCard() == null || !user.getIdCard().contains("*")) oldUser.setIdCard(user.getIdCard());
        // 身份证号为空说明本次表单没有提交该字段,不能覆盖数据库中的历史值;
        // 脱敏值同样只用于回显,不能写回数据库。
        if (!StringUtils.isEmpty(user.getIdCard()) && !user.getIdCard().contains("*")) {
            oldUser.setIdCard(user.getIdCard());
        }
        oldUser.setSex(user.getSex());
        oldUser.setEmail(user.getEmail());
        if (user.getAvatar()!=null && (!user.getAvatar().equals("")))
@@ -456,10 +475,13 @@
        if (user.getStoreIds() != null) {
            List<TStore> assignedStores = validateStores(user.getStoreIds());
            if (assignedStores == null) return ResultUtil.error("所选门店不存在、已停用或不在当前账号管理范围内");
            replaceStoreRelations(oldUser.getId(), assignedStores);
            if (!assignedStores.isEmpty()) {
            // 非超级管理员的页面只会展示自己有权限的门店。保存时仅更新这个范围,
            // 被编辑员工在其他门店(当前账号无权查看)的归属必须原样保留。
            List<TStore> effectiveStores = mergeEditableAndProtectedStores(oldUser.getId(), assignedStores);
            replaceStoreRelations(oldUser.getId(), effectiveStores);
            if (!effectiveStores.isEmpty()) {
                oldUser.setObjectType(3);
                oldUser.setObjectId(assignedStores.get(0).getId());
                oldUser.setObjectId(effectiveStores.get(0).getId());
                oldUser.setRoleid("3");
            } else if (Integer.valueOf(3).equals(oldUser.getObjectType())) {
                // 清除旧字段关联,避免已移除的首个门店仍被当作该员工所属门店。
@@ -468,11 +490,30 @@
        }
        // 门店关联会维护员工归属信息,但不能覆盖编辑页明确选择的角色。
        // 必须在关联处理后写入,否则有门店的员工会被上面的兼容逻辑强制改回角色 3。
        if (!StringUtils.isEmpty(user.getRoleid())) {
        // 个人资料页不提供 roleid_ 回显控件时,旧版前端可能提交字符串
        // "undefined";该值绝不能覆盖 sys_user.roleid。
        if (isValidRoleId(user.getRoleid())) {
            oldUser.setRoleid(user.getRoleid());
            // 仅内置平台管理员(1)和门店管理员(3)切换数据归属类型;
            // 其他自定义角色只控制菜单权限,不改变账号归属。objectId 始终保留。
            syncObjectTypeWithBuiltInRole(oldUser, user.getRoleid());
        }
        this.userService.updateById(oldUser);
        return SUCCESS_TIP;
    }
    private boolean isValidRoleId(String roleId) {
        return !StringUtils.isEmpty(roleId)
                && !"undefined".equalsIgnoreCase(roleId)
                && !"null".equalsIgnoreCase(roleId);
    }
    private void syncObjectTypeWithBuiltInRole(User user, String roleId) {
        if ("1".equals(roleId)) {
            user.setObjectType(1);
        } else if ("3".equals(roleId)) {
            user.setObjectType(3);
        }
    }
    private String maskPhone(String value) {
@@ -493,18 +534,53 @@
    private List<TStore> validateStores(List<Integer> storeIds) {
        List<TStore> stores = new ArrayList<TStore>();
        Integer currentType = UserExt.getUser().getObjectType();
        Integer currentObjectId = UserExt.getUser().getObjectId();
        User currentUser = UserExt.getUser();
        if (storeIds == null) return stores;
        Set<Integer> manageableStoreIds = null;
        if (!isSuperAdmin(currentUser)) {
            List<Integer> assignedStoreIds = userMapper.getStoreIdsByUserId(currentUser.getId());
            manageableStoreIds = assignedStoreIds == null ? Collections.<Integer>emptySet() : new HashSet<>(assignedStoreIds);
        }
        for (Integer storeId : storeIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList())) {
            TStore store = storeService.getById(storeId);
            if (store == null || !Integer.valueOf(1).equals(store.getState())) return null;
            if ((Integer.valueOf(2).equals(currentType) && !currentObjectId.equals(store.getOperatorId()))
                    || (Integer.valueOf(3).equals(currentType) && !currentObjectId.equals(store.getId()))) return null;
            if (manageableStoreIds != null && !manageableStoreIds.contains(storeId)) return null;
            stores.add(store);
        }
        return stores;
    }
    /**
     * 合并本次可编辑范围内的选择与原有不可编辑门店,避免局部管理员覆盖其他门店的归属。
     */
    private List<TStore> mergeEditableAndProtectedStores(Integer userId, List<TStore> editableStores) {
        if (isSuperAdmin(UserExt.getUser())) {
            return editableStores;
        }
        List<Integer> currentUserStoreIds = userMapper.getStoreIdsByUserId(UserExt.getUser().getId());
        Set<Integer> editableStoreIds = currentUserStoreIds == null
                ? Collections.<Integer>emptySet() : new HashSet<>(currentUserStoreIds);
        Map<Integer, TStore> effectiveStoreMap = new LinkedHashMap<>();
        for (TStore store : editableStores) {
            effectiveStoreMap.put(store.getId(), store);
        }
        List<Integer> previousStoreIds = userMapper.getStoreIdsByUserId(userId);
        if (previousStoreIds != null) {
            for (Integer storeId : previousStoreIds) {
                if (storeId == null || editableStoreIds.contains(storeId)) {
                    continue;
                }
                TStore store = storeService.getById(storeId);
                if (store != null) {
                    effectiveStoreMap.put(storeId, store);
                }
            }
        }
        return new ArrayList<>(effectiveStoreMap.values());
    }
    /** 每条关联记录单独生成雪花主键,避免 MyBatis foreach 复用同一个绑定变量。 */
    private void replaceStoreRelations(Integer userId, List<TStore> stores) {
        userMapper.deleteStoreRelations(userId);