mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 22:57:17 +08:00 
			
		
		
		
	feat: 新增用户批量导入功能 (#78)
This commit is contained in:
		| @@ -71,6 +71,11 @@ public class CacheConstants { | |||||||
|      */ |      */ | ||||||
|     public static final String USER_PASSWORD_ERROR_KEY_PREFIX = USER_KEY_PREFIX + "PASSWORD_ERROR" + DELIMITER; |     public static final String USER_PASSWORD_ERROR_KEY_PREFIX = USER_KEY_PREFIX + "PASSWORD_ERROR" + DELIMITER; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 数据导入临时会话key | ||||||
|  |      */ | ||||||
|  |     public static final String DATA_IMPORT_KEY = "SYSTEM" + DELIMITER + "DATA_IMPORT" + DELIMITER; | ||||||
|  |  | ||||||
|     private CacheConstants() { |     private CacheConstants() { | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -19,8 +19,15 @@ package top.continew.admin.common.util; | |||||||
| import cn.hutool.core.codec.Base64; | import cn.hutool.core.codec.Base64; | ||||||
| import cn.hutool.crypto.SecureUtil; | import cn.hutool.crypto.SecureUtil; | ||||||
| import cn.hutool.crypto.asymmetric.KeyType; | import cn.hutool.crypto.asymmetric.KeyType; | ||||||
|  | import cn.hutool.extra.spring.SpringUtil; | ||||||
| import top.continew.admin.common.config.properties.RsaProperties; | import top.continew.admin.common.config.properties.RsaProperties; | ||||||
|  | import top.continew.starter.core.exception.BusinessException; | ||||||
| import top.continew.starter.core.util.validate.ValidationUtils; | import top.continew.starter.core.util.validate.ValidationUtils; | ||||||
|  | import top.continew.starter.security.crypto.autoconfigure.CryptoProperties; | ||||||
|  | import top.continew.starter.security.crypto.encryptor.AesEncryptor; | ||||||
|  | import top.continew.starter.security.crypto.encryptor.IEncryptor; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.stream.Collectors; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 加密/解密工具类 |  * 加密/解密工具类 | ||||||
| @@ -33,29 +40,6 @@ public class SecureUtils { | |||||||
|     private SecureUtils() { |     private SecureUtils() { | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 公钥加密 |  | ||||||
|      * |  | ||||||
|      * @param data      要加密的内容 |  | ||||||
|      * @param publicKey 公钥 |  | ||||||
|      * @return 公钥加密并 Base64 加密后的内容 |  | ||||||
|      */ |  | ||||||
|     public static String encryptByRsaPublicKey(String data, String publicKey) { |  | ||||||
|         return Base64.encode(SecureUtil.rsa(null, publicKey).encrypt(data, KeyType.PublicKey)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 公钥加密 |  | ||||||
|      * |  | ||||||
|      * @param data 要加密的内容 |  | ||||||
|      * @return 公钥加密并 Base64 加密后的内容 |  | ||||||
|      */ |  | ||||||
|     public static String encryptByRsaPublicKey(String data) { |  | ||||||
|         String publicKey = RsaProperties.PUBLIC_KEY; |  | ||||||
|         ValidationUtils.throwIfBlank(publicKey, "请配置 RSA 公钥"); |  | ||||||
|         return encryptByRsaPublicKey(data, publicKey); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 私钥解密 |      * 私钥解密 | ||||||
|      * |      * | ||||||
| @@ -78,4 +62,22 @@ public class SecureUtils { | |||||||
|     public static String decryptByRsaPrivateKey(String data, String privateKey) { |     public static String decryptByRsaPrivateKey(String data, String privateKey) { | ||||||
|         return new String(SecureUtil.rsa(privateKey, null).decrypt(Base64.decode(data), KeyType.PrivateKey)); |         return new String(SecureUtil.rsa(privateKey, null).decrypt(Base64.decode(data), KeyType.PrivateKey)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 对普通加密字段列表进行AES加密,优化starter加密模块后优化这个方法 | ||||||
|  |      * | ||||||
|  |      * @param values 待加密内容 | ||||||
|  |      * @return 加密后内容 | ||||||
|  |      */ | ||||||
|  |     public static List<String> encryptFieldByAes(List<String> values) { | ||||||
|  |         IEncryptor encryptor = new AesEncryptor(); | ||||||
|  |         CryptoProperties properties = SpringUtil.getBean(CryptoProperties.class); | ||||||
|  |         return values.stream().map(value -> { | ||||||
|  |             try { | ||||||
|  |                 return encryptor.encrypt(value, properties.getPassword(), properties.getPublicKey()); | ||||||
|  |             } catch (Exception e) { | ||||||
|  |                 throw new BusinessException("字段加密异常"); | ||||||
|  |             } | ||||||
|  |         }).collect(Collectors.toList()); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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; | ||||||
|  | } | ||||||
| @@ -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; | ||||||
|  | } | ||||||
| @@ -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; | ||||||
|  | } | ||||||
| @@ -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; | ||||||
|  | } | ||||||
| @@ -40,4 +40,20 @@ public interface DeptService extends BaseService<DeptResp, DeptResp, DeptQuery, | |||||||
|      * @return 子部门列表 |      * @return 子部门列表 | ||||||
|      */ |      */ | ||||||
|     List<DeptDO> listChildren(Long id); |     List<DeptDO> listChildren(Long id); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 通过名称查询部门 | ||||||
|  |      * | ||||||
|  |      * @param list 名称列表 | ||||||
|  |      * @return 部门列表 | ||||||
|  |      */ | ||||||
|  |     List<DeptDO> listByNames(List<String> list); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 通过名称查询部门数量 | ||||||
|  |      * | ||||||
|  |      * @param deptNames 名称列表 | ||||||
|  |      * @return 部门数量 | ||||||
|  |      */ | ||||||
|  |     int countByNames(List<String> deptNames); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -67,4 +67,20 @@ public interface RoleService extends BaseService<RoleResp, RoleDetailResp, RoleQ | |||||||
|      * @return 角色信息 |      * @return 角色信息 | ||||||
|      */ |      */ | ||||||
|     RoleDO getByCode(String code); |     RoleDO getByCode(String code); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 根据角色名称查询 | ||||||
|  |      * | ||||||
|  |      * @param list 名称列表 | ||||||
|  |      * @return 角色列表 | ||||||
|  |      */ | ||||||
|  |     List<RoleDO> listByNames(List<String> list); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 根据角色名称查询数量 | ||||||
|  |      * | ||||||
|  |      * @param roleNames 名称列表 | ||||||
|  |      * @return 角色数量 | ||||||
|  |      */ | ||||||
|  |     int countByNames(List<String> roleNames); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -16,6 +16,8 @@ | |||||||
|  |  | ||||||
| package top.continew.admin.system.service; | package top.continew.admin.system.service; | ||||||
|  |  | ||||||
|  | import top.continew.admin.system.model.entity.UserRoleDO; | ||||||
|  |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -42,6 +44,13 @@ public interface UserRoleService { | |||||||
|      */ |      */ | ||||||
|     void deleteByUserIds(List<Long> userIds); |     void deleteByUserIds(List<Long> userIds); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 批量插入 | ||||||
|  |      * | ||||||
|  |      * @param list 数据集 | ||||||
|  |      */ | ||||||
|  |     void saveBatch(List<UserRoleDO> list); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 根据用户 ID 查询 |      * 根据用户 ID 查询 | ||||||
|      * |      * | ||||||
|   | |||||||
| @@ -16,14 +16,14 @@ | |||||||
|  |  | ||||||
| package top.continew.admin.system.service; | package top.continew.admin.system.service; | ||||||
|  |  | ||||||
|  | import jakarta.servlet.http.HttpServletResponse; | ||||||
| import org.springframework.web.multipart.MultipartFile; | import org.springframework.web.multipart.MultipartFile; | ||||||
| import top.continew.admin.system.model.entity.UserDO; | import top.continew.admin.system.model.entity.UserDO; | ||||||
| import top.continew.admin.system.model.query.UserQuery; | import top.continew.admin.system.model.query.UserQuery; | ||||||
| import top.continew.admin.system.model.req.UserBasicInfoUpdateReq; | import top.continew.admin.system.model.req.*; | ||||||
| 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.resp.UserDetailResp; | 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.model.resp.UserResp; | ||||||
| import top.continew.starter.data.mybatis.plus.service.IService; | import top.continew.starter.data.mybatis.plus.service.IService; | ||||||
| import top.continew.starter.extension.crud.service.BaseService; | import top.continew.starter.extension.crud.service.BaseService; | ||||||
| @@ -138,4 +138,23 @@ public interface UserService extends BaseService<UserResp, UserDetailResp, UserQ | |||||||
|      * @return 用户数量 |      * @return 用户数量 | ||||||
|      */ |      */ | ||||||
|     Long countByDeptIds(List<Long> deptIds); |     Long countByDeptIds(List<Long> deptIds); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 下载用户导入模板 | ||||||
|  |      */ | ||||||
|  |     void downloadImportUserTemplate(HttpServletResponse response) throws IOException; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 导入用户 | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |     UserImportResp importUser(UserImportReq req); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 解析用户导入数据 | ||||||
|  |      * | ||||||
|  |      * @param file 导入用户文件 | ||||||
|  |      * @return 解析结果 | ||||||
|  |      */ | ||||||
|  |     UserImportParseResp parseImportUser(MultipartFile file); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ import cn.hutool.core.collection.CollUtil; | |||||||
| import cn.hutool.core.util.ObjectUtil; | import cn.hutool.core.util.ObjectUtil; | ||||||
| import cn.hutool.extra.spring.SpringUtil; | import cn.hutool.extra.spring.SpringUtil; | ||||||
| import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; | import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; | ||||||
|  | import com.baomidou.mybatisplus.core.toolkit.Wrappers; | ||||||
| import jakarta.annotation.Resource; | import jakarta.annotation.Resource; | ||||||
| import lombok.RequiredArgsConstructor; | import lombok.RequiredArgsConstructor; | ||||||
| import org.springframework.stereotype.Service; | 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.data.core.util.MetaUtils; | ||||||
| import top.continew.starter.extension.crud.service.impl.BaseServiceImpl; | import top.continew.starter.extension.crud.service.impl.BaseServiceImpl; | ||||||
|  |  | ||||||
| import java.util.ArrayList; | import java.util.*; | ||||||
| import java.util.List; |  | ||||||
| import java.util.Optional; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 部门业务实现 |  * 部门业务实现 | ||||||
| @@ -62,6 +61,22 @@ public class DeptServiceImpl extends BaseServiceImpl<DeptMapper, DeptDO, DeptRes | |||||||
|         return baseMapper.lambdaQuery().apply(databaseType.findInSet(id, "ancestors")).list(); |         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 |     @Override | ||||||
|     protected void beforeAdd(DeptReq req) { |     protected void beforeAdd(DeptReq req) { | ||||||
|         String name = req.getName(); |         String name = req.getName(); | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ import cn.hutool.core.bean.BeanUtil; | |||||||
| import cn.hutool.core.collection.CollUtil; | import cn.hutool.core.collection.CollUtil; | ||||||
| import cn.hutool.core.util.ObjectUtil; | import cn.hutool.core.util.ObjectUtil; | ||||||
| import com.alicp.jetcache.anno.CacheInvalidate; | import com.alicp.jetcache.anno.CacheInvalidate; | ||||||
|  | import com.baomidou.mybatisplus.core.toolkit.Wrappers; | ||||||
| import lombok.RequiredArgsConstructor; | import lombok.RequiredArgsConstructor; | ||||||
| import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||||
| import org.springframework.transaction.annotation.Transactional; | 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(); |         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)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 名称是否存在 |      * 名称是否存在 | ||||||
|      * |      * | ||||||
|   | |||||||
| @@ -68,6 +68,11 @@ public class UserRoleServiceImpl implements UserRoleService { | |||||||
|         baseMapper.lambdaUpdate().in(UserRoleDO::getUserId, userIds).remove(); |         baseMapper.lambdaUpdate().in(UserRoleDO::getUserId, userIds).remove(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void saveBatch(List<UserRoleDO> list) { | ||||||
|  |         baseMapper.insertBatch(list); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     @ContainerMethod(namespace = ContainerConstants.USER_ROLE_ID_LIST, type = MappingType.ORDER_OF_KEYS) |     @ContainerMethod(namespace = ContainerConstants.USER_ROLE_ID_LIST, type = MappingType.ORDER_OF_KEYS) | ||||||
|     public List<Long> listRoleIdByUserId(Long userId) { |     public List<Long> listRoleIdByUserId(Long userId) { | ||||||
|   | |||||||
| @@ -20,18 +20,30 @@ import cn.dev33.satoken.stp.StpUtil; | |||||||
| import cn.hutool.core.bean.BeanUtil; | import cn.hutool.core.bean.BeanUtil; | ||||||
| import cn.hutool.core.collection.CollUtil; | import cn.hutool.core.collection.CollUtil; | ||||||
| import cn.hutool.core.img.ImgUtil; | 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.io.file.FileNameUtil; | ||||||
|  | import cn.hutool.core.lang.UUID; | ||||||
| import cn.hutool.core.map.MapUtil; | import cn.hutool.core.map.MapUtil; | ||||||
| import cn.hutool.core.util.ObjectUtil; | import cn.hutool.core.util.*; | ||||||
| import cn.hutool.core.util.StrUtil; | 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.CacheInvalidate; | ||||||
| import com.alicp.jetcache.anno.CacheType; | import com.alicp.jetcache.anno.CacheType; | ||||||
| import com.alicp.jetcache.anno.CacheUpdate; | import com.alicp.jetcache.anno.CacheUpdate; | ||||||
| import com.alicp.jetcache.anno.Cached; | import com.alicp.jetcache.anno.Cached; | ||||||
| import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | ||||||
| import com.baomidou.mybatisplus.core.metadata.IPage; | 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.annotation.Resource; | ||||||
|  | import jakarta.servlet.http.HttpServletResponse; | ||||||
| import lombok.RequiredArgsConstructor; | 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.beans.factory.annotation.Value; | ||||||
| import org.springframework.security.crypto.password.PasswordEncoder; | import org.springframework.security.crypto.password.PasswordEncoder; | ||||||
| import org.springframework.stereotype.Service; | 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.CacheConstants; | ||||||
| import top.continew.admin.common.constant.SysConstants; | import top.continew.admin.common.constant.SysConstants; | ||||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | 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.common.util.helper.LoginHelper; | ||||||
| import top.continew.admin.system.mapper.UserMapper; | import top.continew.admin.system.mapper.UserMapper; | ||||||
| import top.continew.admin.system.model.entity.DeptDO; | 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.UserDO; | ||||||
|  | import top.continew.admin.system.model.entity.UserRoleDO; | ||||||
| import top.continew.admin.system.model.query.UserQuery; | import top.continew.admin.system.model.query.UserQuery; | ||||||
| import top.continew.admin.system.model.req.UserBasicInfoUpdateReq; | import top.continew.admin.system.model.req.*; | ||||||
| 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.resp.UserDetailResp; | 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.model.resp.UserResp; | ||||||
| import top.continew.admin.system.service.*; | 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.constant.StringConstants; | ||||||
|  | import top.continew.starter.core.exception.BusinessException; | ||||||
| import top.continew.starter.core.util.validate.CheckUtils; | 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.PageQuery; | ||||||
| import top.continew.starter.extension.crud.model.query.SortQuery; | 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.CommonUserService; | ||||||
| import top.continew.starter.extension.crud.service.impl.BaseServiceImpl; | import top.continew.starter.extension.crud.service.impl.BaseServiceImpl; | ||||||
|  |  | ||||||
|  | import java.io.BufferedInputStream; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
|  | import java.time.Duration; | ||||||
| import java.time.LocalDateTime; | import java.time.LocalDateTime; | ||||||
| import java.util.*; | import java.util.*; | ||||||
|  | import java.util.function.Function; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
|  |  | ||||||
|  | import static top.continew.admin.system.enums.ImportPolicyEnum.*; | ||||||
| import static top.continew.admin.system.enums.PasswordPolicyEnum.*; | 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 PasswordEncoder passwordEncoder; | ||||||
|     private final OptionService optionService; |     private final OptionService optionService; | ||||||
|     private final UserPasswordHistoryService userPasswordHistoryService; |     private final UserPasswordHistoryService userPasswordHistoryService; | ||||||
|  |     private final RoleService roleService; | ||||||
|  |  | ||||||
|     @Resource |     @Resource | ||||||
|     private DeptService deptService; |     private DeptService deptService; | ||||||
|     @Value("${avatar.support-suffix}") |     @Value("${avatar.support-suffix}") | ||||||
| @@ -104,6 +127,176 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes | |||||||
|         return user.getId(); |         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 |     @Override | ||||||
|     @Transactional(rollbackFor = Exception.class) |     @Transactional(rollbackFor = Exception.class) | ||||||
|     @CacheUpdate(key = "#id", value = "#req.nickname", name = CacheConstants.USER_KEY_PREFIX) |     @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); |         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); |         Long count = baseMapper.selectCountByPhone(phone, id); | ||||||
|         return null != count && count > 0; |         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)); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -22,19 +22,21 @@ import io.swagger.v3.oas.annotations.Operation; | |||||||
| import io.swagger.v3.oas.annotations.Parameter; | import io.swagger.v3.oas.annotations.Parameter; | ||||||
| import io.swagger.v3.oas.annotations.enums.ParameterIn; | import io.swagger.v3.oas.annotations.enums.ParameterIn; | ||||||
| import io.swagger.v3.oas.annotations.tags.Tag; | import io.swagger.v3.oas.annotations.tags.Tag; | ||||||
|  | import jakarta.servlet.http.HttpServletResponse; | ||||||
|  | import jakarta.validation.constraints.NotNull; | ||||||
|  | import lombok.RequiredArgsConstructor; | ||||||
|  | import org.springframework.http.MediaType; | ||||||
| import org.springframework.validation.annotation.Validated; | import org.springframework.validation.annotation.Validated; | ||||||
| import org.springframework.web.bind.annotation.PatchMapping; | import org.springframework.web.bind.annotation.*; | ||||||
| import org.springframework.web.bind.annotation.PathVariable; | import org.springframework.web.multipart.MultipartFile; | ||||||
| import org.springframework.web.bind.annotation.RequestBody; |  | ||||||
| import org.springframework.web.bind.annotation.RestController; |  | ||||||
| import top.continew.admin.common.constant.RegexConstants; | import top.continew.admin.common.constant.RegexConstants; | ||||||
| import top.continew.admin.common.util.SecureUtils; | import top.continew.admin.common.util.SecureUtils; | ||||||
| import top.continew.admin.system.model.query.UserQuery; | import top.continew.admin.system.model.query.UserQuery; | ||||||
|  | import top.continew.admin.system.model.req.UserImportReq; | ||||||
| import top.continew.admin.system.model.req.UserPasswordResetReq; | import top.continew.admin.system.model.req.UserPasswordResetReq; | ||||||
| import top.continew.admin.system.model.req.UserReq; | import top.continew.admin.system.model.req.UserReq; | ||||||
| import top.continew.admin.system.model.req.UserRoleUpdateReq; | import top.continew.admin.system.model.req.UserRoleUpdateReq; | ||||||
| import top.continew.admin.system.model.resp.UserDetailResp; | import top.continew.admin.system.model.resp.*; | ||||||
| import top.continew.admin.system.model.resp.UserResp; |  | ||||||
| import top.continew.admin.system.service.UserService; | import top.continew.admin.system.service.UserService; | ||||||
| import top.continew.starter.core.util.ExceptionUtils; | import top.continew.starter.core.util.ExceptionUtils; | ||||||
| import top.continew.starter.core.util.validate.ValidationUtils; | import top.continew.starter.core.util.validate.ValidationUtils; | ||||||
| @@ -44,6 +46,8 @@ import top.continew.starter.extension.crud.enums.Api; | |||||||
| import top.continew.starter.extension.crud.util.ValidateGroup; | import top.continew.starter.extension.crud.util.ValidateGroup; | ||||||
| import top.continew.starter.web.model.R; | import top.continew.starter.web.model.R; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 用户管理 API |  * 用户管理 API | ||||||
|  * |  * | ||||||
| @@ -53,9 +57,12 @@ import top.continew.starter.web.model.R; | |||||||
| @Tag(name = "用户管理 API") | @Tag(name = "用户管理 API") | ||||||
| @Validated | @Validated | ||||||
| @RestController | @RestController | ||||||
|  | @RequiredArgsConstructor | ||||||
| @CrudRequestMapping(value = "/system/user", api = {Api.PAGE, Api.GET, Api.ADD, Api.UPDATE, Api.DELETE, Api.EXPORT}) | @CrudRequestMapping(value = "/system/user", api = {Api.PAGE, Api.GET, Api.ADD, Api.UPDATE, Api.DELETE, Api.EXPORT}) | ||||||
| public class UserController extends BaseController<UserService, UserResp, UserDetailResp, UserQuery, UserReq> { | public class UserController extends BaseController<UserService, UserResp, UserDetailResp, UserQuery, UserReq> { | ||||||
|  |  | ||||||
|  |     private final UserService userService; | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public R<Long> add(@Validated(ValidateGroup.Crud.Add.class) @RequestBody UserReq req) { |     public R<Long> add(@Validated(ValidateGroup.Crud.Add.class) @RequestBody UserReq req) { | ||||||
|         String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getPassword())); |         String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getPassword())); | ||||||
| @@ -66,6 +73,28 @@ public class UserController extends BaseController<UserService, UserResp, UserDe | |||||||
|         return super.add(req); |         return super.add(req); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Operation(summary = "下载用户导入模板", description = "下载用户导入模板") | ||||||
|  |     @SaCheckPermission("system:user:import") | ||||||
|  |     @GetMapping(value = "downloadImportUserTemplate", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) | ||||||
|  |     public void downloadImportUserTemplate(HttpServletResponse response) throws IOException { | ||||||
|  |         userService.downloadImportUserTemplate(response); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Operation(summary = "解析用户导入数据", description = "解析用户导入数据") | ||||||
|  |     @SaCheckPermission("system:user:import") | ||||||
|  |     @PostMapping(value = "parseImportUser") | ||||||
|  |     public R<UserImportParseResp> parseImportUser(@NotNull(message = "文件不能为空") MultipartFile file) { | ||||||
|  |         ValidationUtils.throwIf(file::isEmpty, "文件不能为空"); | ||||||
|  |         return R.ok(userService.parseImportUser(file)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Operation(summary = "导入用户", description = "导入用户") | ||||||
|  |     @SaCheckPermission("system:user:import") | ||||||
|  |     @PostMapping(value = "import") | ||||||
|  |     public R<UserImportResp> importUser(@Validated @RequestBody UserImportReq req) { | ||||||
|  |         return R.ok(userService.importUser(req)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Operation(summary = "重置密码", description = "重置用户登录密码") |     @Operation(summary = "重置密码", description = "重置用户登录密码") | ||||||
|     @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) |     @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) | ||||||
|     @SaCheckPermission("system:user:resetPwd") |     @SaCheckPermission("system:user:resetPwd") | ||||||
|   | |||||||
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user
	 kils
					kils