package cn.iocoder.yudao.module.system.service.user;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollectionUtil;
|
import cn.hutool.core.util.ObjUtil;
|
import cn.hutool.core.util.StrUtil;
|
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
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.UserImportRespVO;
|
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;
|
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.service.dept.DeptService;
|
import cn.iocoder.yudao.module.system.service.dept.PostService;
|
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
|
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
|
import cn.iocoder.yudao.module.system.service.tenant.TenantService;
|
import cn.iocoder.yudao.framework.operatelog.core.util.OperateLogUtils;
|
import com.google.common.annotations.VisibleForTesting;
|
import com.mzt.logapi.context.LogRecordContext;
|
import com.mzt.logapi.service.impl.DiffParseFunction;
|
import com.mzt.logapi.starter.annotation.LogRecord;
|
import jakarta.annotation.Resource;
|
import jakarta.validation.ConstraintViolationException;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.context.annotation.Lazy;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import java.time.LocalDateTime;
|
import java.util.*;
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
|
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
|
import static cn.iocoder.yudao.module.system.enums.LogRecordConstants.*;
|
|
/**
|
* 后台用户 Service 实现类
|
*
|
* @author 超级管理员
|
*/
|
@Service("adminUserService")
|
@Slf4j
|
public class AdminUserServiceImpl implements AdminUserService {
|
|
static final String USER_INIT_PASSWORD_KEY = "system.user.init-password";
|
|
static final String USER_REGISTER_ENABLED_KEY = "system.user.register-enabled";
|
|
@Resource
|
private AdminUserMapper userMapper;
|
|
@Resource
|
private DeptService deptService;
|
@Resource
|
private PostService postService;
|
@Resource
|
private PermissionService permissionService;
|
@Resource
|
private PasswordEncoder passwordEncoder;
|
@Resource
|
@Lazy // 延迟,避免循环依赖报错
|
private TenantService tenantService;
|
@Resource
|
@Lazy // 懒加载,避免循环依赖
|
private OAuth2TokenService oauth2TokenService;
|
|
@Resource
|
private UserPostMapper userPostMapper;
|
|
@Resource
|
private ConfigApi configApi;
|
|
@Resource
|
private AdminUserProducer adminUserProducer;
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
@LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_CREATE_SUB_TYPE, bizNo = "{{#user.id}}",
|
success = SYSTEM_USER_CREATE_SUCCESS)
|
public Long createUser(UserSaveReqVO createReqVO) {
|
// 1.1 校验账户配合
|
tenantService.handleTenantInfo(tenant -> {
|
long count = userMapper.selectCount();
|
if (count >= tenant.getAccountCount()) {
|
throw exception(USER_COUNT_MAX, tenant.getAccountCount());
|
}
|
});
|
// 1.2 校验正确性
|
validateUserForCreateOrUpdate(null, createReqVO.getUsername(),
|
createReqVO.getMobile(), createReqVO.getEmail(), createReqVO.getDeptId(), createReqVO.getPostIds());
|
// 2.1 插入用户
|
AdminUserDO user = BeanUtils.toBean(createReqVO, AdminUserDO.class);
|
user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启
|
user.setPassword(encodePassword(createReqVO.getPassword())); // 加密密码
|
userMapper.insert(user);
|
// 2.2 插入关联岗位
|
if (CollectionUtil.isNotEmpty(user.getPostIds())) {
|
userPostMapper.insertBatch(convertList(user.getPostIds(),
|
postId -> new UserPostDO().setUserId(user.getId()).setPostId(postId)));
|
}
|
// 2.3 分配用户角色
|
if (CollUtil.isNotEmpty(createReqVO.getRoleIds())) {
|
permissionService.assignUserRole(user.getId(), createReqVO.getRoleIds());
|
}
|
|
// 3. 记录操作日志上下文
|
LogRecordContext.putVariable("user", user);
|
return user.getId();
|
}
|
|
@Override
|
public Long registerUser(AuthRegisterReqVO registerReqVO) {
|
// 1.1 校验是否开启注册
|
if (ObjUtil.notEqual(configApi.getConfigValueByKey(USER_REGISTER_ENABLED_KEY), "true")) {
|
throw exception(USER_REGISTER_DISABLED);
|
}
|
// 1.2 校验账户配合
|
tenantService.handleTenantInfo(tenant -> {
|
long count = userMapper.selectCount();
|
if (count >= tenant.getAccountCount()) {
|
throw exception(USER_COUNT_MAX, tenant.getAccountCount());
|
}
|
});
|
// 1.3 校验正确性
|
validateUserForCreateOrUpdate(null, registerReqVO.getUsername(), null, null, null, null);
|
|
// 2. 插入用户
|
AdminUserDO user = BeanUtils.toBean(registerReqVO, AdminUserDO.class);
|
user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启
|
user.setPassword(encodePassword(registerReqVO.getPassword())); // 加密密码
|
userMapper.insert(user);
|
return user.getId();
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
@LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
|
success = SYSTEM_USER_UPDATE_SUCCESS)
|
public void updateUser(UserSaveReqVO updateReqVO) {
|
updateReqVO.setPassword(null); // 特殊:此处不更新密码
|
// 1. 校验正确性
|
AdminUserDO oldUser = validateUserForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getUsername(),
|
updateReqVO.getMobile(), updateReqVO.getEmail(), updateReqVO.getDeptId(), updateReqVO.getPostIds());
|
|
// 2.1 更新用户
|
AdminUserDO updateObj = BeanUtils.toBean(updateReqVO, AdminUserDO.class);
|
userMapper.updateById(updateObj);
|
// 2.2 更新岗位
|
updateUserPost(updateReqVO, updateObj);
|
// 2.3 昵称 / 头像变化时,发送消息供下游订阅(如 IM 模块推 FRIEND_INFO_UPDATED)
|
publishUserProfileUpdatedIfChanged(oldUser, updateReqVO.getNickname(), updateReqVO.getAvatar());
|
|
// 3. 记录操作日志上下文
|
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldUser, UserSaveReqVO.class));
|
LogRecordContext.putVariable("user", oldUser);
|
}
|
|
private void updateUserPost(UserSaveReqVO reqVO, AdminUserDO updateObj) {
|
Long userId = reqVO.getId();
|
Set<Long> dbPostIds = convertSet(userPostMapper.selectListByUserId(userId), UserPostDO::getPostId);
|
// 计算新增和删除的岗位编号
|
Set<Long> postIds = CollUtil.emptyIfNull(updateObj.getPostIds());
|
Collection<Long> createPostIds = CollUtil.subtract(postIds, dbPostIds);
|
Collection<Long> deletePostIds = CollUtil.subtract(dbPostIds, postIds);
|
// 执行新增和删除。对于已经授权的岗位,不用做任何处理
|
if (!CollectionUtil.isEmpty(createPostIds)) {
|
userPostMapper.insertBatch(convertList(createPostIds,
|
postId -> new UserPostDO().setUserId(userId).setPostId(postId)));
|
}
|
if (!CollectionUtil.isEmpty(deletePostIds)) {
|
userPostMapper.deleteByUserIdAndPostId(userId, deletePostIds);
|
}
|
}
|
|
@Override
|
public void updateUserLogin(Long id, String loginIp) {
|
userMapper.updateById(new AdminUserDO().setId(id).setLoginIp(loginIp).setLoginDate(LocalDateTime.now()));
|
}
|
|
@Override
|
public void updateUserProfile(Long id, UserProfileUpdateReqVO reqVO) {
|
// 1. 校验正确性
|
AdminUserDO oldUser = validateUserExists(id);
|
validateEmailUnique(id, reqVO.getEmail());
|
validateMobileUnique(id, reqVO.getMobile());
|
|
// 2. 执行更新
|
userMapper.updateById(BeanUtils.toBean(reqVO, AdminUserDO.class).setId(id));
|
|
// 3. 昵称 / 头像变化时,发送消息供下游订阅(如 IM 模块推 FRIEND_INFO_UPDATED)
|
publishUserProfileUpdatedIfChanged(oldUser, reqVO.getNickname(), reqVO.getAvatar());
|
}
|
|
/**
|
* 仅当 nickname 或 avatar 跟旧值不一致时,发送 AdminUserProfileUpdateMessage
|
*/
|
private void publishUserProfileUpdatedIfChanged(AdminUserDO oldUser, String newNickname, String newAvatar) {
|
boolean nicknameChanged = newNickname != null && !ObjUtil.equal(oldUser.getNickname(), newNickname);
|
boolean avatarChanged = newAvatar != null && !ObjUtil.equal(oldUser.getAvatar(), newAvatar);
|
if (!nicknameChanged && !avatarChanged) {
|
return;
|
}
|
adminUserProducer.sendUserProfileUpdateMessage(oldUser.getId(),
|
nicknameChanged ? newNickname : null,
|
avatarChanged ? newAvatar : null);
|
}
|
|
@Override
|
public void updateUserPassword(Long id, UserProfileUpdatePasswordReqVO reqVO) {
|
// 校验旧密码密码
|
validateOldPassword(id, reqVO.getOldPassword());
|
// 执行更新
|
AdminUserDO updateObj = new AdminUserDO().setId(id);
|
updateObj.setPassword(encodePassword(reqVO.getNewPassword())); // 加密密码
|
userMapper.updateById(updateObj);
|
}
|
|
@Override
|
@LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_UPDATE_PASSWORD_SUB_TYPE, bizNo = "{{#id}}",
|
success = SYSTEM_USER_UPDATE_PASSWORD_SUCCESS)
|
public void updateUserPassword(Long id, String password) {
|
// 1. 校验用户存在
|
AdminUserDO user = validateUserExists(id);
|
|
// 2. 更新密码
|
AdminUserDO updateObj = new AdminUserDO();
|
updateObj.setId(id);
|
updateObj.setPassword(encodePassword(password)); // 加密密码
|
userMapper.updateById(updateObj);
|
|
// 3. 记录操作日志上下文
|
LogRecordContext.putVariable("user", user);
|
LogRecordContext.putVariable("newPassword", updateObj.getPassword());
|
}
|
|
@Override
|
public void updateUserStatus(Long id, Integer status) {
|
// 校验用户存在
|
validateUserExists(id);
|
// 更新状态
|
AdminUserDO updateObj = new AdminUserDO();
|
updateObj.setId(id);
|
updateObj.setStatus(status);
|
userMapper.updateById(updateObj);
|
|
// 如果是禁用用户,则删除其 Token 信息
|
if (CommonStatusEnum.isDisable(status)) {
|
oauth2TokenService.removeAccessToken(id, UserTypeEnum.ADMIN.getValue());
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
@LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_DELETE_SUB_TYPE, bizNo = "{{#id}}",
|
success = SYSTEM_USER_DELETE_SUCCESS)
|
public void deleteUser(Long id) {
|
// 1. 校验用户存在
|
AdminUserDO user = validateUserExists(id);
|
|
// 2.1 删除用户
|
userMapper.deleteById(id);
|
// 2.2 删除用户关联数据
|
permissionService.processUserDeleted(id);
|
// 2.2 删除用户岗位
|
userPostMapper.deleteByUserId(id);
|
|
// 3. 记录操作日志上下文
|
LogRecordContext.putVariable("user", user);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void deleteUserList(List<Long> ids) {
|
// 1. 批量删除用户
|
userMapper.deleteByIds(ids);
|
|
// 2. 批量删除用户关联数据
|
ids.forEach(id -> {
|
permissionService.processUserDeleted(id);
|
userPostMapper.deleteByUserId(id);
|
});
|
}
|
|
@Override
|
public AdminUserDO getUserByUsername(String username) {
|
return userMapper.selectByUsername(username);
|
}
|
|
@Override
|
public AdminUserDO getUserByMobile(String mobile) {
|
return userMapper.selectByMobile(mobile);
|
}
|
|
@Override
|
public PageResult<AdminUserDO> getUserPage(UserPageReqVO reqVO) {
|
// 如果有角色编号,查询角色对应的用户编号
|
Set<Long> userIds = null;
|
if (reqVO.getRoleId() != null) {
|
userIds = permissionService.getUserRoleIdListByRoleId(singleton(reqVO.getRoleId()));
|
if (CollUtil.isEmpty(userIds)) {
|
return PageResult.empty();
|
}
|
}
|
|
// 分页查询
|
return userMapper.selectPage(reqVO, getDeptCondition(reqVO.getDeptId()), userIds);
|
}
|
|
@Override
|
public AdminUserDO getUser(Long id) {
|
return userMapper.selectById(id);
|
}
|
|
@Override
|
public List<AdminUserDO> getUserListByDeptIds(Collection<Long> deptIds) {
|
if (CollUtil.isEmpty(deptIds)) {
|
return Collections.emptyList();
|
}
|
return userMapper.selectListByDeptIds(deptIds);
|
}
|
|
@Override
|
public List<AdminUserDO> getUserListByPostIds(Collection<Long> postIds) {
|
if (CollUtil.isEmpty(postIds)) {
|
return Collections.emptyList();
|
}
|
Set<Long> userIds = convertSet(userPostMapper.selectListByPostIds(postIds), UserPostDO::getUserId);
|
if (CollUtil.isEmpty(userIds)) {
|
return Collections.emptyList();
|
}
|
return userMapper.selectByIds(userIds);
|
}
|
|
@Override
|
public List<AdminUserDO> getUserList(Collection<Long> ids) {
|
if (CollUtil.isEmpty(ids)) {
|
return Collections.emptyList();
|
}
|
return userMapper.selectByIds(ids);
|
}
|
|
public List<AdminUserDO> getUserListAll() {
|
return userMapper.selectList();
|
}
|
|
|
@Override
|
public void validateUserList(Collection<Long> ids) {
|
if (CollUtil.isEmpty(ids)) {
|
return;
|
}
|
// 获得岗位信息
|
List<AdminUserDO> users = userMapper.selectByIds(ids);
|
Map<Long, AdminUserDO> userMap = CollectionUtils.convertMap(users, AdminUserDO::getId);
|
// 校验
|
ids.forEach(id -> {
|
AdminUserDO user = userMap.get(id);
|
if (user == null) {
|
throw exception(USER_NOT_EXISTS);
|
}
|
if (!CommonStatusEnum.ENABLE.getStatus().equals(user.getStatus())) {
|
throw exception(USER_IS_DISABLE, user.getNickname());
|
}
|
});
|
}
|
|
@Override
|
public List<AdminUserDO> getUserListByNickname(String nickname) {
|
return userMapper.selectListByNickname(nickname);
|
}
|
|
/**
|
* 获得部门条件:查询指定部门的子部门编号们,包括自身
|
*
|
* @param deptId 部门编号
|
* @return 部门编号集合
|
*/
|
private Set<Long> getDeptCondition(Long deptId) {
|
if (deptId == null) {
|
return Collections.emptySet();
|
}
|
Set<Long> deptIds = convertSet(deptService.getChildDeptList(deptId), DeptDO::getId);
|
deptIds.add(deptId); // 包括自身
|
return deptIds;
|
}
|
|
private AdminUserDO validateUserForCreateOrUpdate(Long id, String username, String mobile, String email,
|
Long deptId, Set<Long> postIds) {
|
// 关闭数据权限,避免因为没有数据权限,查询不到数据,进而导致唯一校验不正确
|
return DataPermissionUtils.executeIgnore(() -> {
|
// 校验用户存在
|
AdminUserDO user = validateUserExists(id);
|
// 校验用户名唯一
|
validateUsernameUnique(id, username);
|
// 校验手机号唯一
|
validateMobileUnique(id, mobile);
|
// 校验邮箱唯一
|
validateEmailUnique(id, email);
|
// 校验部门处于开启状态
|
deptService.validateDeptList(CollectionUtils.singleton(deptId));
|
// 校验岗位处于开启状态
|
postService.validatePostList(postIds);
|
return user;
|
});
|
}
|
|
@VisibleForTesting
|
AdminUserDO validateUserExists(Long id) {
|
if (id == null) {
|
return null;
|
}
|
AdminUserDO user = userMapper.selectById(id);
|
if (user == null) {
|
throw exception(USER_NOT_EXISTS);
|
}
|
return user;
|
}
|
|
@VisibleForTesting
|
void validateUsernameUnique(Long id, String username) {
|
if (StrUtil.isBlank(username)) {
|
return;
|
}
|
AdminUserDO user = userMapper.selectByUsername(username);
|
if (user == null) {
|
return;
|
}
|
// 如果 id 为空,说明不用比较是否为相同 id 的用户
|
if (id == null) {
|
throw exception(USER_USERNAME_EXISTS);
|
}
|
if (!user.getId().equals(id)) {
|
throw exception(USER_USERNAME_EXISTS);
|
}
|
}
|
|
@VisibleForTesting
|
void validateEmailUnique(Long id, String email) {
|
if (StrUtil.isBlank(email)) {
|
return;
|
}
|
AdminUserDO user = userMapper.selectByEmail(email);
|
if (user == null) {
|
return;
|
}
|
// 如果 id 为空,说明不用比较是否为相同 id 的用户
|
if (id == null) {
|
throw exception(USER_EMAIL_EXISTS);
|
}
|
if (!user.getId().equals(id)) {
|
throw exception(USER_EMAIL_EXISTS);
|
}
|
}
|
|
@VisibleForTesting
|
void validateMobileUnique(Long id, String mobile) {
|
if (StrUtil.isBlank(mobile)) {
|
return;
|
}
|
AdminUserDO user = userMapper.selectByMobile(mobile);
|
if (user == null) {
|
return;
|
}
|
// 如果 id 为空,说明不用比较是否为相同 id 的用户
|
if (id == null) {
|
throw exception(USER_MOBILE_EXISTS);
|
}
|
if (!user.getId().equals(id)) {
|
throw exception(USER_MOBILE_EXISTS);
|
}
|
}
|
|
/**
|
* 校验旧密码
|
* @param id 用户 id
|
* @param oldPassword 旧密码
|
*/
|
@VisibleForTesting
|
void validateOldPassword(Long id, String oldPassword) {
|
AdminUserDO user = userMapper.selectById(id);
|
if (user == null) {
|
throw exception(USER_NOT_EXISTS);
|
}
|
if (!isPasswordMatch(oldPassword, user.getPassword())) {
|
throw exception(USER_PASSWORD_FAILED);
|
}
|
}
|
|
@Override
|
@LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_IMPORT_SUB_TYPE, bizNo = "0",
|
success = SYSTEM_USER_IMPORT_SUCCESS)
|
@Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
|
public UserImportRespVO importUserList(List<UserImportExcelVO> importUsers, boolean isUpdateSupport) {
|
// 1.1 参数校验
|
if (CollUtil.isEmpty(importUsers)) {
|
throw exception(USER_IMPORT_LIST_IS_EMPTY);
|
}
|
// 1.2 初始化密码:Excel 的「初始密码」列留空时,使用系统配置的初始密码
|
String initPassword = configApi.getConfigValueByKey(USER_INIT_PASSWORD_KEY);
|
// 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<>())
|
.updateUsernames(new ArrayList<>()).failureUsernames(new LinkedHashMap<>()).build();
|
AtomicInteger index = new AtomicInteger(1);
|
importUsers.forEach(importUser -> {
|
int currentIndex = index.getAndIncrement();
|
String username = StrUtil.trim(importUser.getUsername());
|
// 失败明细的 key 定位到具体的行(并带上用户账号),方便用户在 Excel 中找到这一行
|
String rowKey = StrUtil.format("第 {} 行{}", currentIndex,
|
StrUtil.isEmpty(username) ? "" : StrUtil.format("「{}」", username));
|
try {
|
// 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(username);
|
if (existUser == null) {
|
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) {
|
throw new ServiceException(USER_USERNAME_EXISTS.getCode(),
|
"用户账号已存在。如需覆盖这个用户,请勾选「是否覆盖」后重新导入;否则请修改 Excel 中的用户账号");
|
}
|
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));
|
}
|
});
|
// 记录导入结果汇总,供 @LogRecord 的 success 文案引用
|
OperateLogUtils.putImportSummary(respVO.getCreateUsernames(), respVO.getUpdateUsernames(),
|
respVO.getFailureUsernames());
|
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);
|
}
|
|
@Override
|
public List<AdminUserDO> getDeptUsers(Collection<Long> deptIds) {
|
return userMapper.selectListByDeptIds(deptIds);
|
}
|
|
@Override
|
public boolean isPasswordMatch(String rawPassword, String encodedPassword) {
|
return passwordEncoder.matches(rawPassword, encodedPassword);
|
}
|
|
/**
|
* 对密码进行加密
|
*
|
* @param password 密码
|
* @return 加密后的密码
|
*/
|
private String encodePassword(String password) {
|
return passwordEncoder.encode(password);
|
}
|
|
}
|