feat: 新增用户批量导入功能 (#78)

This commit is contained in:
kils
2024-06-19 16:52:09 +08:00
committed by GitHub
parent b512ea99f3
commit c2ad055cf8
17 changed files with 831 additions and 42 deletions

View File

@@ -0,0 +1,57 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.enums;
import cn.hutool.core.collection.CollUtil;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.starter.data.mybatis.plus.base.IBaseEnum;
import java.util.List;
/**
* 数据导入策略
*
* @author Kils
* @since 2024-06-17 18:33
*/
@Getter
@RequiredArgsConstructor
public enum ImportPolicyEnum implements IBaseEnum<Integer> {
/**
* 跳过该行
*/
SKIP(1, "跳过该行"),
/**
* 修改数据
*/
UPDATE(2, "修改数据"),
/**
* 停止导入
*/
EXIT(3, "停止导入");
private final Integer value;
private final String description;
public boolean validate(ImportPolicyEnum importPolicy, String data, List<String> existList) {
return this == importPolicy && CollUtil.isNotEmpty(existList) && existList.contains(data);
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.system.enums.ImportPolicyEnum;
import top.continew.starter.extension.crud.model.req.BaseReq;
import java.io.Serial;
/**
* 用户导入参数
*
* @author Kils
* @since 2024-6-17 16:42
*/
@Data
@Schema(description = "用户导入参数")
public class UserImportReq extends BaseReq {
@Serial
private static final long serialVersionUID = 1L;
/**
* 导入会话KEY
*/
@Schema(description = "导入会话KEY", example = "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed")
@NotBlank(message = "导入已过期,请重新上传")
private String importKey;
/**
* 用户重复策略
*/
@Schema(description = "重复用户策略", type = "Integer", allowableValues = {"1", "2", "3", "4"}, example = "1")
@NotNull(message = "重复用户策略不能为空")
private ImportPolicyEnum duplicateUser;
/**
* 重复邮箱策略
*/
@Schema(description = "重复邮箱策略", type = "Integer", allowableValues = {"1", "2", "3", "4"}, example = "1")
@NotNull(message = "重复邮箱策略不能为空")
private ImportPolicyEnum duplicateEmail;
/**
* 重复手机策略
*/
@Schema(description = "重复手机策略", type = "Integer", allowableValues = {"1", "2", "3", "4"}, example = "1")
@NotNull(message = "重复手机策略不能为空")
private ImportPolicyEnum duplicatePhone;
/**
* 默认状态
*/
@Schema(description = "默认状态1启用2禁用", type = "Integer", allowableValues = {"1", "2"}, example = "1")
private DisEnableStatusEnum defaultStatus;
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.model.req;
import cn.hutool.core.lang.RegexPool;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import top.continew.admin.common.constant.RegexConstants;
import top.continew.starter.extension.crud.model.req.BaseReq;
import top.continew.starter.extension.crud.util.ValidateGroup;
import java.io.Serial;
/**
* 用户导入行数据
*
* @author Kils
* @since 2024-6-17 16:42
*/
@Data
@Schema(description = "用户导入行数据")
public class UserImportRowReq extends BaseReq {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空")
@Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4-64 个字符,支持大小写字母、数字、下划线,以字母开头")
private String username;
/**
* 昵称
*/
@NotBlank(message = "昵称不能为空")
@Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线")
private String nickname;
/**
* 密码
*/
@NotBlank(message = "密码不能为空", groups = ValidateGroup.Crud.Add.class)
private String password;
/**
* 部门名称
*/
@NotNull(message = "所属部门不能为空")
private String deptName;
/**
* 角色
*/
@NotBlank(message = "所属角色不能为空")
private String roleName;
/**
* 性别
*/
private String gender;
/**
* 邮箱
*/
@Pattern(regexp = "^$|" + RegexPool.EMAIL, message = "邮箱格式错误")
@Length(max = 255, message = "邮箱长度不能超过 {max} 个字符")
private String email;
/**
* 手机号码
*/
@Pattern(regexp = "^$|" + RegexPool.MOBILE, message = "手机号码格式错误")
private String phone;
/**
* 描述
*/
private String description;
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.model.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户导入结果
*
* @author kils
* @since 2024-06-18 14:37
*/
@Data
@Schema(description = "用户导入结果")
@AllArgsConstructor
@NoArgsConstructor
public class UserImportParseResp {
private static final long serialVersionUID = 1L;
/**
* 导入会话KEY
*/
@Schema(description = "导入会话KEY", example = "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed")
private String importKey;
/**
* 总计行数
*/
@Schema(description = "总计行数", example = "100")
private Integer totalRows;
/**
* 有效行数
*/
@Schema(description = "有效行数", example = "100")
private Integer validRows;
/**
* 用户重复行数
*/
@Schema(description = "用户重复行数", example = "100")
private Integer duplicateUserRows;
/**
* 重复邮箱行数
*/
@Schema(description = "重复邮箱行数", example = "100")
private Integer duplicateEmailRows;
/**
* 重复手机行数
*/
@Schema(description = "重复手机行数", example = "100")
private Integer duplicatePhoneRows;
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.model.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户导入结果
*
* @author kils
* @since 2024-06-18 14:37
*/
@Data
@Schema(description = "用户导入结果")
@AllArgsConstructor
@NoArgsConstructor
public class UserImportResp {
private static final long serialVersionUID = 1L;
/**
* 总计行数
*/
@Schema(description = "总计行数", example = "100")
private Integer totalRows;
/**
* 新增行数
*/
@Schema(description = "新增行数", example = "100")
private Integer insertRows;
/**
* 修改行数
*/
@Schema(description = "修改行数", example = "100")
private Integer updateRows;
}

View File

@@ -40,4 +40,20 @@ public interface DeptService extends BaseService<DeptResp, DeptResp, DeptQuery,
* @return 子部门列表
*/
List<DeptDO> listChildren(Long id);
/**
* 通过名称查询部门
*
* @param list 名称列表
* @return 部门列表
*/
List<DeptDO> listByNames(List<String> list);
/**
* 通过名称查询部门数量
*
* @param deptNames 名称列表
* @return 部门数量
*/
int countByNames(List<String> deptNames);
}

View File

@@ -67,4 +67,20 @@ public interface RoleService extends BaseService<RoleResp, RoleDetailResp, RoleQ
* @return 角色信息
*/
RoleDO getByCode(String code);
/**
* 根据角色名称查询
*
* @param list 名称列表
* @return 角色列表
*/
List<RoleDO> listByNames(List<String> list);
/**
* 根据角色名称查询数量
*
* @param roleNames 名称列表
* @return 角色数量
*/
int countByNames(List<String> roleNames);
}

View File

@@ -16,6 +16,8 @@
package top.continew.admin.system.service;
import top.continew.admin.system.model.entity.UserRoleDO;
import java.util.List;
/**
@@ -42,6 +44,13 @@ public interface UserRoleService {
*/
void deleteByUserIds(List<Long> userIds);
/**
* 批量插入
*
* @param list 数据集
*/
void saveBatch(List<UserRoleDO> list);
/**
* 根据用户 ID 查询
*

View File

@@ -16,14 +16,14 @@
package top.continew.admin.system.service;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.multipart.MultipartFile;
import top.continew.admin.system.model.entity.UserDO;
import top.continew.admin.system.model.query.UserQuery;
import top.continew.admin.system.model.req.UserBasicInfoUpdateReq;
import top.continew.admin.system.model.req.UserPasswordResetReq;
import top.continew.admin.system.model.req.UserReq;
import top.continew.admin.system.model.req.UserRoleUpdateReq;
import top.continew.admin.system.model.req.*;
import top.continew.admin.system.model.resp.UserDetailResp;
import top.continew.admin.system.model.resp.UserImportParseResp;
import top.continew.admin.system.model.resp.UserImportResp;
import top.continew.admin.system.model.resp.UserResp;
import top.continew.starter.data.mybatis.plus.service.IService;
import top.continew.starter.extension.crud.service.BaseService;
@@ -138,4 +138,23 @@ public interface UserService extends BaseService<UserResp, UserDetailResp, UserQ
* @return 用户数量
*/
Long countByDeptIds(List<Long> deptIds);
/**
* 下载用户导入模板
*/
void downloadImportUserTemplate(HttpServletResponse response) throws IOException;
/**
* 导入用户
*
*/
UserImportResp importUser(UserImportReq req);
/**
* 解析用户导入数据
*
* @param file 导入用户文件
* @return 解析结果
*/
UserImportParseResp parseImportUser(MultipartFile file);
}

View File

@@ -20,6 +20,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -37,9 +38,7 @@ import top.continew.starter.data.core.enums.DatabaseType;
import top.continew.starter.data.core.util.MetaUtils;
import top.continew.starter.extension.crud.service.impl.BaseServiceImpl;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.*;
/**
* 部门业务实现
@@ -62,6 +61,22 @@ public class DeptServiceImpl extends BaseServiceImpl<DeptMapper, DeptDO, DeptRes
return baseMapper.lambdaQuery().apply(databaseType.findInSet(id, "ancestors")).list();
}
@Override
public List<DeptDO> listByNames(List<String> list) {
if (CollUtil.isEmpty(list)) {
return Collections.emptyList();
}
return this.list(Wrappers.<DeptDO>lambdaQuery().in(DeptDO::getName, list));
}
@Override
public int countByNames(List<String> deptNames) {
if (CollUtil.isEmpty(deptNames)) {
return 0;
}
return (int)this.count(Wrappers.<DeptDO>lambdaQuery().in(DeptDO::getName, deptNames));
}
@Override
protected void beforeAdd(DeptReq req) {
String name = req.getName();

View File

@@ -22,6 +22,7 @@ import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alicp.jetcache.anno.CacheInvalidate;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -161,6 +162,22 @@ public class RoleServiceImpl extends BaseServiceImpl<RoleMapper, RoleDO, RoleRes
return baseMapper.lambdaQuery().eq(RoleDO::getCode, code).one();
}
@Override
public List<RoleDO> listByNames(List<String> list) {
if (CollUtil.isEmpty(list)) {
return Collections.emptyList();
}
return this.list(Wrappers.<RoleDO>lambdaQuery().in(RoleDO::getName, list));
}
@Override
public int countByNames(List<String> roleNames) {
if (CollUtil.isEmpty(roleNames)) {
return 0;
}
return (int)this.count(Wrappers.<RoleDO>lambdaQuery().in(RoleDO::getName, roleNames));
}
/**
* 名称是否存在
*

View File

@@ -68,6 +68,11 @@ public class UserRoleServiceImpl implements UserRoleService {
baseMapper.lambdaUpdate().in(UserRoleDO::getUserId, userIds).remove();
}
@Override
public void saveBatch(List<UserRoleDO> list) {
baseMapper.insertBatch(list);
}
@Override
@ContainerMethod(namespace = ContainerConstants.USER_ROLE_ID_LIST, type = MappingType.ORDER_OF_KEYS)
public List<Long> listRoleIdByUserId(Long userId) {

View File

@@ -20,18 +20,30 @@ import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.*;
import cn.hutool.extra.validation.ValidationUtil;
import cn.hutool.http.ContentType;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel;
import com.alicp.jetcache.anno.CacheInvalidate;
import com.alicp.jetcache.anno.CacheType;
import com.alicp.jetcache.anno.CacheUpdate;
import com.alicp.jetcache.anno.Cached;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import me.ahoo.cosid.IdGenerator;
import me.ahoo.cosid.provider.DefaultIdGeneratorProvider;
import net.dreamlu.mica.core.result.R;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@@ -41,19 +53,24 @@ import top.continew.admin.auth.service.OnlineUserService;
import top.continew.admin.common.constant.CacheConstants;
import top.continew.admin.common.constant.SysConstants;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.common.enums.GenderEnum;
import top.continew.admin.common.util.SecureUtils;
import top.continew.admin.common.util.helper.LoginHelper;
import top.continew.admin.system.mapper.UserMapper;
import top.continew.admin.system.model.entity.DeptDO;
import top.continew.admin.system.model.entity.RoleDO;
import top.continew.admin.system.model.entity.UserDO;
import top.continew.admin.system.model.entity.UserRoleDO;
import top.continew.admin.system.model.query.UserQuery;
import top.continew.admin.system.model.req.UserBasicInfoUpdateReq;
import top.continew.admin.system.model.req.UserPasswordResetReq;
import top.continew.admin.system.model.req.UserReq;
import top.continew.admin.system.model.req.UserRoleUpdateReq;
import top.continew.admin.system.model.req.*;
import top.continew.admin.system.model.resp.UserDetailResp;
import top.continew.admin.system.model.resp.UserImportParseResp;
import top.continew.admin.system.model.resp.UserImportResp;
import top.continew.admin.system.model.resp.UserResp;
import top.continew.admin.system.service.*;
import top.continew.starter.cache.redisson.util.RedisUtils;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.exception.BusinessException;
import top.continew.starter.core.util.validate.CheckUtils;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.query.SortQuery;
@@ -61,11 +78,15 @@ import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.extension.crud.service.CommonUserService;
import top.continew.starter.extension.crud.service.impl.BaseServiceImpl;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import static top.continew.admin.system.enums.ImportPolicyEnum.*;
import static top.continew.admin.system.enums.PasswordPolicyEnum.*;
/**
@@ -83,6 +104,8 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
private final PasswordEncoder passwordEncoder;
private final OptionService optionService;
private final UserPasswordHistoryService userPasswordHistoryService;
private final RoleService roleService;
@Resource
private DeptService deptService;
@Value("${avatar.support-suffix}")
@@ -104,6 +127,176 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
return user.getId();
}
@Override
public void downloadImportUserTemplate(HttpServletResponse response) throws IOException {
try {
BufferedInputStream inputStream = FileUtil.getInputStream("templates/import/userImportTemplate.xlsx");
byte[] bytes = IoUtil.readBytes(inputStream);
response.setHeader("Content-Disposition", "attachment;filename=" + URLUtil.encode("用户导入模板.xlsx"));
response.addHeader("Content-Length", String.valueOf(bytes.length));
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
response.setContentType("application/octet-stream;charset=UTF-8");
IoUtil.write(response.getOutputStream(), true, bytes);
} catch (Exception e) {
log.error("下载用户导入模板失败:", e);
response.setCharacterEncoding(CharsetUtil.UTF_8);
response.setContentType(ContentType.JSON.toString());
response.getWriter().write(JSONUtil.toJsonStr(R.fail("下载用户导入模板失败")));
}
}
@Override
public UserImportParseResp parseImportUser(MultipartFile file) {
UserImportParseResp userImportResp = new UserImportParseResp();
List<UserImportRowReq> userRowList;
// 读取表格数据
try {
userRowList = EasyExcel.read(file.getInputStream())
.head(UserImportRowReq.class)
.sheet()
.headRowNumber(1)
.doReadSync();
} catch (Exception e) {
log.error("用户导入数据文件解析异常:", e);
throw new BusinessException("数据文件解析异常");
}
userImportResp.setTotalRows(userRowList.size());
if (CollUtil.isEmpty(userRowList)) {
throw new BusinessException("数据文件格式错误");
}
// 过滤无效数据
List<UserImportRowReq> validUserRowList = filterErrorUserImportData(userRowList);
userImportResp.setValidRows(validUserRowList.size());
if (CollUtil.isEmpty(validUserRowList)) {
throw new BusinessException("数据文件格式错误");
}
// 检测表格内数据是否合法
Set<String> seenEmails = new HashSet<>();
boolean hasDuplicateEmail = validUserRowList.stream()
.map(UserImportRowReq::getEmail)
.anyMatch(email -> email != null && !seenEmails.add(email));
CheckUtils.throwIf(hasDuplicateEmail, "存在重复邮箱,请检测数据");
Set<String> seenPhones = new HashSet<>();
boolean hasDuplicatePhone = validUserRowList.stream()
.map(UserImportRowReq::getPhone)
.anyMatch(phone -> phone != null && !seenPhones.add(phone));
CheckUtils.throwIf(hasDuplicatePhone, "存在重复手机,请检测数据");
// 校验是否存在错误角色
List<String> roleNames = validUserRowList.stream().map(UserImportRowReq::getRoleName).distinct().toList();
int existRoleCount = roleService.countByNames(roleNames);
CheckUtils.throwIf(existRoleCount < roleNames.size(), "存在错误角色,请检查数据");
// 校验是否存在错误部门
List<String> deptNames = validUserRowList.stream().map(UserImportRowReq::getDeptName).distinct().toList();
int existDeptCount = deptService.countByNames(deptNames);
CheckUtils.throwIf(existDeptCount < deptNames.size(), "存在错误部门,请检查数据");
// 查询重复用户
userImportResp
.setDuplicateUserRows(countExistByField(validUserRowList, UserImportRowReq::getUsername, UserDO::getUsername, false));
// 查询重复邮箱
userImportResp
.setDuplicateEmailRows(countExistByField(validUserRowList, UserImportRowReq::getEmail, UserDO::getEmail, true));
// 查询重复手机
userImportResp
.setDuplicatePhoneRows(countExistByField(validUserRowList, UserImportRowReq::getPhone, UserDO::getPhone, true));
// 设置导入会话并缓存数据有效期10分钟
String importKey = UUID.fastUUID().toString(true);
RedisUtils.set(CacheConstants.DATA_IMPORT_KEY + importKey, JSONUtil.toJsonStr(validUserRowList), Duration
.ofMinutes(10));
userImportResp.setImportKey(importKey);
return userImportResp;
}
@Override
@Transactional(rollbackFor = Exception.class)
public UserImportResp importUser(UserImportReq req) {
// 校验导入会话是否过期
List<UserImportRowReq> importUserList;
try {
String data = RedisUtils.get(CacheConstants.DATA_IMPORT_KEY + req.getImportKey());
importUserList = JSONUtil.toList(data, UserImportRowReq.class);
CheckUtils.throwIf(CollUtil.isEmpty(importUserList), "导入已过期,请重新上传");
} catch (Exception e) {
log.error("导入异常:", e);
throw new BusinessException("导入已过期,请重新上传");
}
// 已存在数据查询
List<String> existEmails = listExistByField(importUserList, UserImportRowReq::getEmail, UserDO::getEmail);
List<String> existPhones = listExistByField(importUserList, UserImportRowReq::getPhone, UserDO::getPhone);
List<UserDO> existUserList = listByUsernames(importUserList.stream()
.map(UserImportRowReq::getUsername)
.filter(Objects::nonNull)
.toList());
List<String> existUsernames = existUserList.stream().map(UserDO::getUsername).toList();
CheckUtils
.throwIf(isExitImportUser(req, importUserList, existUsernames, existEmails, existPhones), "数据不符合导入策略,已退出导入");
// 基础数据准备
Map<String, Long> userMap = existUserList.stream()
.collect(Collectors.toMap(UserDO::getUsername, UserDO::getId));
List<RoleDO> roleList = roleService.listByNames(importUserList.stream()
.map(UserImportRowReq::getRoleName)
.distinct()
.toList());
Map<String, Long> roleMap = roleList.stream().collect(Collectors.toMap(RoleDO::getName, RoleDO::getId));
List<DeptDO> deptList = deptService.listByNames(importUserList.stream()
.map(UserImportRowReq::getDeptName)
.distinct()
.toList());
Map<String, Long> deptMap = deptList.stream().collect(Collectors.toMap(DeptDO::getName, DeptDO::getId));
// 批量操作数据库集合
List<UserDO> insertList = new ArrayList<>();
List<UserDO> updateList = new ArrayList<>();
List<UserRoleDO> userRoleDOList = new ArrayList<>();
// ID生成器
IdGenerator idGenerator = DefaultIdGeneratorProvider.INSTANCE.getShare();
for (UserImportRowReq row : importUserList) {
if (isSkipUserImport(req, row, existUsernames, existPhones, existEmails)) {
// 按规则跳过该行
continue;
}
UserDO userDO = BeanUtil.toBeanIgnoreError(row, UserDO.class);
userDO.setStatus(req.getDefaultStatus());
userDO.setPwdResetTime(LocalDateTime.now());
userDO.setGender(EnumUtil.getBy(GenderEnum::getDescription, row.getGender(), GenderEnum.UNKNOWN));
userDO.setDeptId(deptMap.get(row.getDeptName()));
// 修改 or 新增
if (UPDATE.validate(req.getDuplicateUser(), row.getUsername(), existUsernames)) {
userDO.setId(userMap.get(row.getUsername()));
updateList.add(userDO);
} else {
userDO.setId(idGenerator.generate());
userDO.setIsSystem(false);
insertList.add(userDO);
}
userRoleDOList.add(new UserRoleDO(userDO.getId(), roleMap.get(row.getRoleName())));
}
doImportUser(insertList, updateList, userRoleDOList);
RedisUtils.delete(CacheConstants.DATA_IMPORT_KEY + req.getImportKey());
return new UserImportResp(insertList.size() + updateList.size(), insertList.size(), updateList.size());
}
@Transactional(rollbackFor = Exception.class)
public void doImportUser(List<UserDO> insertList, List<UserDO> updateList, List<UserRoleDO> userRoleDOList) {
if (CollUtil.isNotEmpty(insertList)) {
baseMapper.insertBatch(insertList);
}
if (CollUtil.isNotEmpty(updateList)) {
this.updateBatchById(updateList);
userRoleService.deleteByUserIds(updateList.stream().map(UserDO::getId).toList());
}
if (CollUtil.isNotEmpty(userRoleDOList)) {
userRoleService.saveBatch(userRoleDOList);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
@CacheUpdate(key = "#id", value = "#req.nickname", name = CacheConstants.USER_KEY_PREFIX)
@@ -316,6 +509,104 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
userRoleService.add(req.getRoleIds(), userId);
}
/**
* 判断是否跳过导入
*
* @param req 导入参数
* @param row 导入数据
* @param existUsernames 导入数据中已存在的用户名
* @param existEmails 导入数据中已存在的邮箱
* @param existPhones 导入数据中已存在的手机号
* @return 是否跳过
*/
private boolean isSkipUserImport(UserImportReq req,
UserImportRowReq row,
List<String> existUsernames,
List<String> existPhones,
List<String> existEmails) {
return SKIP.validate(req.getDuplicateUser(), row.getUsername(), existUsernames) || SKIP.validate(req
.getDuplicateEmail(), row.getEmail(), existEmails) || SKIP.validate(req.getDuplicatePhone(), row
.getPhone(), existPhones);
}
/**
* 判断是否退出导入
*
* @param req 导入参数
* @param list 导入数据
* @param existUsernames 导入数据中已存在的用户名
* @param existEmails 导入数据中已存在的邮箱
* @param existPhones 导入数据中已存在的手机号
* @return 是否退出
*/
private boolean isExitImportUser(UserImportReq req,
List<UserImportRowReq> list,
List<String> existUsernames,
List<String> existEmails,
List<String> existPhones) {
return list.stream()
.anyMatch(row -> EXIT.validate(req.getDuplicateUser(), row.getUsername(), existUsernames) || EXIT
.validate(req.getDuplicateEmail(), row.getEmail(), existEmails) || EXIT.validate(req
.getDuplicatePhone(), row.getPhone(), existPhones));
}
/**
* 按指定数据集获取数据库已存在的数量
*
* @param userRowList 导入的数据源
* @param rowField 导入数据的字段
* @param dbField 对比数据库的字段
* @return 存在的数量
*/
private int countExistByField(List<UserImportRowReq> userRowList,
Function<UserImportRowReq, String> rowField,
SFunction<UserDO, ?> dbField,
boolean fieldEncrypt) {
List<String> fieldValues = userRowList.stream().map(rowField).filter(Objects::nonNull).toList();
if (fieldValues.isEmpty()) {
return 0;
}
return (int)this.count(Wrappers.<UserDO>lambdaQuery()
.in(dbField, fieldEncrypt ? SecureUtils.encryptFieldByAes(fieldValues) : fieldValues));
}
/**
* 按指定数据集获取数据库已存在内容
*
* @param userRowList 导入的数据源
* @param rowField 导入数据的字段
* @param dbField 对比数据库的字段
* @return 存在的内容
*/
private List<String> listExistByField(List<UserImportRowReq> userRowList,
Function<UserImportRowReq, String> rowField,
SFunction<UserDO, String> dbField) {
List<String> fieldValues = userRowList.stream().map(rowField).filter(Objects::nonNull).toList();
if (fieldValues.isEmpty()) {
return Collections.emptyList();
}
List<UserDO> userDOList = baseMapper.selectList(Wrappers.<UserDO>lambdaQuery()
.in(dbField, SecureUtils.encryptFieldByAes(fieldValues))
.select(dbField));
return userDOList.stream().map(dbField).filter(Objects::nonNull).toList();
}
/**
* 过滤无效的导入用户数据,批量导入不严格校验数据
*/
private List<UserImportRowReq> filterErrorUserImportData(List<UserImportRowReq> userImportList) {
// 校验过滤
List<UserImportRowReq> list = userImportList.stream()
.filter(row -> ValidationUtil.validate(row).size() == 0)
.toList();
// 用户名去重
return list.stream()
.collect(Collectors.toMap(UserImportRowReq::getUsername, user -> user, (existing, replacement) -> existing))
.values()
.stream()
.toList();
}
/**
* 检测密码合法性
*
@@ -373,4 +664,10 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
Long count = baseMapper.selectCountByPhone(phone, id);
return null != count && count > 0;
}
private List<UserDO> listByUsernames(List<String> usernames) {
return this.list(Wrappers.<UserDO>lambdaQuery()
.in(UserDO::getUsername, usernames)
.select(UserDO::getId, UserDO::getUsername));
}
}