diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/BasePageRequest.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/BasePageRequest.java index 83ec140508..13c1b6b088 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/BasePageRequest.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/BasePageRequest.java @@ -3,6 +3,7 @@ package io.metersphere.sdk.dto; import com.google.common.base.CaseFormat; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; @@ -21,6 +22,7 @@ public class BasePageRequest { private int current; @Min(value = 5, message = "每页显示条数必须不小于5") + @Max(value = 100, message = "每页显示条数不能大于100") @Schema(title = "每页显示条数") private int pageSize; diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/ExcelParseDTO.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/ExcelParseDTO.java new file mode 100644 index 0000000000..8977c22336 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/ExcelParseDTO.java @@ -0,0 +1,25 @@ +package io.metersphere.sdk.dto; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; +import java.util.TreeMap; + +@Data +public class ExcelParseDTO { + private List dataList = new ArrayList<>(); + private TreeMap errRowData = new TreeMap<>(); + + public void addRowData(T t) { + dataList.add(t); + } + + public void addErrorRowData(Integer index, T t) { + errRowData.put(index, t); + } + + public void addErrorRowDataAll(TreeMap errRowData) { + this.errRowData.putAll(errRowData); + } +} diff --git a/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties b/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties index 00de2d2dc8..ebd8e8930f 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties @@ -1,3 +1,4 @@ +excel.parse.error=Excel parse error role.not.global.system=Role is not global system role role.not.contains.member=Role not contains member user.not.login=User not login diff --git a/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties b/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties index 35a953bbd7..653c5aa981 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties @@ -1,3 +1,4 @@ +excel.parse.error=Excel解析失败 role.not.global.system=角色不是全局系统角色 role.not.contains.member=角色不包含系统成员角色 user.not.login=未获取到登录用户 diff --git a/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties b/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties index 6297ea5bad..e017d9dc2e 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties @@ -1,3 +1,4 @@ +excel.parse.error=Excel解析失敗 role.not.global.system=角色不是為全局系統角色 role.not.contains.member=角色不包含系統成員角色 user.not.login=未獲取到登錄用戶 diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/UserController.java b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/UserController.java index 1b1567aca5..6092ecbdb1 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/UserController.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/UserController.java @@ -14,9 +14,10 @@ import io.metersphere.sdk.util.PageUtils; import io.metersphere.sdk.util.Pager; import io.metersphere.sdk.util.SessionUtils; import io.metersphere.system.dto.UserBatchCreateDTO; -import io.metersphere.system.dto.UserEditEnableRequest; -import io.metersphere.system.dto.UserEditRequest; import io.metersphere.system.dto.UserRoleOption; +import io.metersphere.system.dto.request.UserEditEnableRequest; +import io.metersphere.system.dto.request.UserEditRequest; +import io.metersphere.system.dto.response.UserImportResponse; import io.metersphere.system.dto.response.UserTableResponse; import io.metersphere.system.service.GlobalUserRoleService; import io.metersphere.system.service.UserService; @@ -27,6 +28,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -53,7 +55,7 @@ public class UserController { @PostMapping("/add") @RequiresPermissions(PermissionConstants.SYSTEM_USER_READ_ADD) public UserBatchCreateDTO addUser(@Validated({Created.class}) @RequestBody UserBatchCreateDTO userCreateDTO) { - return userService.addBatch(userCreateDTO, UserSourceEnum.LOCAL.name(), SessionUtils.getUserId()); + return userService.addUser(userCreateDTO, UserSourceEnum.LOCAL.name(), SessionUtils.getUserId()); } @PostMapping("/update") @@ -77,4 +79,10 @@ public class UserController { public UserEditEnableRequest updateUserEnable(@Validated @RequestBody UserEditEnableRequest request) { return userService.updateUserEnable(request, SessionUtils.getSessionId()); } + + @PostMapping(value = "/import", consumes = {"multipart/form-data"}) + @RequiresPermissions(PermissionConstants.SYSTEM_USER_READ_IMPORT) + public UserImportResponse importUser(@RequestPart(value = "file", required = false) MultipartFile excelFile) { + return userService.importByExcel(excelFile, UserSourceEnum.LOCAL.name(), SessionUtils.getSessionId()); + } } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/excel/ExcelValidateHelper.java b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/excel/ExcelValidateHelper.java new file mode 100644 index 0000000000..d1b45dbef0 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/excel/ExcelValidateHelper.java @@ -0,0 +1,44 @@ +package io.metersphere.system.dto.excel; + +import com.alibaba.excel.annotation.ExcelProperty; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import jakarta.validation.groups.Default; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Field; +import java.util.Set; + +@Component +public class ExcelValidateHelper { + + private static ExcelValidateHelper excelValidateHelper; + + @Resource + Validator validator; + + public static String validateEntity(T obj) throws NoSuchFieldException { + StringBuilder result = new StringBuilder(); + Set> set = excelValidateHelper.validator.validate(obj, Default.class); + if (set != null && !set.isEmpty()) { + for (ConstraintViolation cv : set) { + Field declaredField = obj.getClass().getDeclaredField(cv.getPropertyPath().toString()); + ExcelProperty annotation = declaredField.getAnnotation(ExcelProperty.class); + //拼接错误信息,包含当前出错数据的标题名字+错误信息 + result.append(annotation.value()[0] + cv.getMessage()).append("; "); + } + } + return result.toString(); + } + + /** + * 在静态方法中调用 + */ + @PostConstruct + public void initialize() { + excelValidateHelper = this; + excelValidateHelper.validator = this.validator; + } +} \ No newline at end of file diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/excel/UserExcel.java b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/excel/UserExcel.java new file mode 100644 index 0000000000..d9a903f9bd --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/excel/UserExcel.java @@ -0,0 +1,27 @@ +package io.metersphere.system.dto.excel; + +import com.alibaba.excel.annotation.ExcelProperty; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class UserExcel { + @NotBlank(message = "{user.name.not_blank}") + @Size(min = 1, max = 255, message = "{user.name.length_range}") + @ExcelProperty("name*") + private String name; + + @NotBlank(message = "{cannot_be_null}") + @Size(min = 1, max = 64, message = "{user.email.length_range}") + @Email(message = "{user.email.invalid}") + @ExcelProperty("email*") + private String email; + + @ExcelProperty("phone") + private String phone; + + @ExcelProperty("workspace") + private String workspaceName; +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/excel/UserExcelRowDTO.java b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/excel/UserExcelRowDTO.java new file mode 100644 index 0000000000..f5d2395a5a --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/excel/UserExcelRowDTO.java @@ -0,0 +1,10 @@ +package io.metersphere.system.dto.excel; + +import lombok.Data; + +@Data +public class UserExcelRowDTO extends UserExcel { + public int dataIndex; + public String errorMessage; + public String userRoleId; +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/UserEditEnableRequest.java b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/request/UserEditEnableRequest.java similarity index 89% rename from backend/services/system-setting/src/main/java/io/metersphere/system/dto/UserEditEnableRequest.java rename to backend/services/system-setting/src/main/java/io/metersphere/system/dto/request/UserEditEnableRequest.java index 6c35991fa2..d3be028860 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/UserEditEnableRequest.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/request/UserEditEnableRequest.java @@ -1,4 +1,4 @@ -package io.metersphere.system.dto; +package io.metersphere.system.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; @@ -11,6 +11,6 @@ public class UserEditEnableRequest { @Schema(title = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED) @NotEmpty(message = "{user.not.empty}") List userIdList; - + boolean enable; } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/UserEditRequest.java b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/request/UserEditRequest.java similarity index 89% rename from backend/services/system-setting/src/main/java/io/metersphere/system/dto/UserEditRequest.java rename to backend/services/system-setting/src/main/java/io/metersphere/system/dto/request/UserEditRequest.java index f6e59b53f0..8c9eed9ad2 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/UserEditRequest.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/request/UserEditRequest.java @@ -1,5 +1,6 @@ -package io.metersphere.system.dto; +package io.metersphere.system.dto.request; +import io.metersphere.system.dto.UserCreateInfo; import io.metersphere.validation.groups.Created; import io.metersphere.validation.groups.Updated; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/response/UserImportResponse.java b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/response/UserImportResponse.java new file mode 100644 index 0000000000..1269f4897b --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/response/UserImportResponse.java @@ -0,0 +1,31 @@ +package io.metersphere.system.dto.response; + +import io.metersphere.sdk.dto.ExcelParseDTO; +import io.metersphere.system.dto.excel.UserExcelRowDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.collections4.MapUtils; + +import java.util.TreeMap; + +@Getter +@Setter +public class UserImportResponse { + @Schema(title = "导入数量") + private int importCount; + @Schema(title = "成功数量") + private int successCount; + @Schema(title = "报错信息") + private TreeMap errorMessages = new TreeMap<>(); + + public void generateResponse(ExcelParseDTO excelParseDTO) { + successCount = excelParseDTO.getDataList().size(); + if (MapUtils.isNotEmpty(excelParseDTO.getErrRowData())) { + excelParseDTO.getErrRowData().forEach((k, v) -> { + errorMessages.put(k, v.getErrorMessage()); + }); + } + importCount = errorMessages.size() + successCount; + } +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/UserService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/UserService.java index c7962ff716..dacbfbee8f 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/service/UserService.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/UserService.java @@ -1,6 +1,8 @@ package io.metersphere.system.service; +import com.alibaba.excel.EasyExcelFactory; import io.metersphere.sdk.dto.BasePageRequest; +import io.metersphere.sdk.dto.ExcelParseDTO; import io.metersphere.sdk.dto.UserDTO; import io.metersphere.sdk.exception.MSException; import io.metersphere.sdk.log.constants.OperationLogModule; @@ -9,22 +11,30 @@ import io.metersphere.sdk.log.service.OperationLogService; import io.metersphere.sdk.mapper.BaseUserMapper; import io.metersphere.sdk.util.BeanUtils; import io.metersphere.sdk.util.CodingUtil; +import io.metersphere.sdk.util.LogUtils; import io.metersphere.sdk.util.Translator; import io.metersphere.system.domain.OperationLog; import io.metersphere.system.domain.User; import io.metersphere.system.domain.UserExample; import io.metersphere.system.dto.UserBatchCreateDTO; import io.metersphere.system.dto.UserCreateInfo; -import io.metersphere.system.dto.UserEditEnableRequest; -import io.metersphere.system.dto.UserEditRequest; +import io.metersphere.system.dto.excel.UserExcel; +import io.metersphere.system.dto.excel.UserExcelRowDTO; +import io.metersphere.system.dto.request.UserEditEnableRequest; +import io.metersphere.system.dto.request.UserEditRequest; +import io.metersphere.system.dto.response.UserImportResponse; import io.metersphere.system.dto.response.UserTableResponse; import io.metersphere.system.mapper.UserMapper; +import io.metersphere.system.utils.UserImportEventListener; import jakarta.annotation.Resource; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import org.apache.commons.collections4.CollectionUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import java.util.ArrayList; import java.util.List; @@ -91,9 +101,13 @@ public class UserService { } } - public UserBatchCreateDTO addBatch(UserBatchCreateDTO userCreateDTO, String source, String operator) { + public UserBatchCreateDTO addUser(UserBatchCreateDTO userCreateDTO, String source, String operator) { this.validateUserInfo(userCreateDTO.getUserInfoList()); globalUserRoleService.checkRoleIsGlobalAndHaveMember(userCreateDTO.getUserRoleIdList(), true); + return this.saveUserAndRole(userCreateDTO, source, operator); + } + + private UserBatchCreateDTO saveUserAndRole(UserBatchCreateDTO userCreateDTO, String source, String operator) { long createTime = System.currentTimeMillis(); List saveUserList = new ArrayList<>(); //添加用户 @@ -110,9 +124,7 @@ public class UserService { userMapper.insertSelective(user); saveUserList.add(user); } - userRoleRelationService.batchSave(userCreateDTO.getUserRoleIdList(), saveUserList); - //写入操作日志 operationLogService.batchAdd(this.getBatchAddLogs(saveUserList)); return userCreateDTO; @@ -184,4 +196,66 @@ public class UserService { throw new MSException(Translator.get("user.not.exist")); } } + + public UserImportResponse importByExcel(MultipartFile excelFile, String source, String sessionId) { + UserImportResponse importResponse = new UserImportResponse(); + try { + ExcelParseDTO excelParseDTO = this.getUserExcelParseDTO(excelFile); + if (CollectionUtils.isNotEmpty(excelParseDTO.getDataList())) { + this.saveUserByExcelData(excelParseDTO.getDataList(), source, sessionId); + } + importResponse.generateResponse(excelParseDTO); + } catch (Exception e) { + LogUtils.info("import user error", e); + } + return importResponse; + } + + public ExcelParseDTO getUserExcelParseDTO(MultipartFile excelFile) throws Exception { + UserImportEventListener userImportEventListener = new UserImportEventListener(); + EasyExcelFactory.read(excelFile.getInputStream(), UserExcel.class, userImportEventListener).sheet().doRead(); + ExcelParseDTO excelParseDTO = this.validateExcelUserInfo(userImportEventListener.getExcelParseDTO()); + return excelParseDTO; + } + + /** + * 校验excel导入的数据是否与数据库中的数据冲突 + * + * @param excelParseDTO + * @return + */ + private ExcelParseDTO validateExcelUserInfo(@Valid @NotNull ExcelParseDTO excelParseDTO) { + List prepareSaveList = excelParseDTO.getDataList(); + if (CollectionUtils.isNotEmpty(prepareSaveList)) { + var userInDbMap = baseUserMapper.selectUserIdByEmailList( + prepareSaveList.stream().map(UserExcelRowDTO::getEmail).collect(Collectors.toList())) + .stream().collect(Collectors.toMap(User::getEmail, User::getId)); + for (UserExcelRowDTO userExcelRow : prepareSaveList) { + //判断邮箱是否已存在数据库中 + if (userInDbMap.containsKey(userExcelRow.getEmail())) { + userExcelRow.setErrorMessage(Translator.get("user.email.repeat") + ": " + userExcelRow.getEmail()); + excelParseDTO.addErrorRowData(userExcelRow.getDataIndex(), userExcelRow); + } + } + excelParseDTO.getDataList().removeAll(excelParseDTO.getErrRowData().values()); + } + return excelParseDTO; + } + + private void saveUserByExcelData(@Valid @NotEmpty List dataList, @Valid @NotEmpty String source, @Valid @NotBlank String sessionId) { + UserBatchCreateDTO userBatchCreateDTO = new UserBatchCreateDTO(); + userBatchCreateDTO.setUserRoleIdList(new ArrayList<>() {{ + add("member"); + }}); + List userCreateInfoList = new ArrayList<>(); + dataList.forEach(userExcelRowDTO -> { + UserCreateInfo userCreateInfo = new UserCreateInfo(); + BeanUtils.copyBean(userCreateInfo, userExcelRowDTO); + userCreateInfoList.add(userCreateInfo); + }); + userBatchCreateDTO.setUserInfoList(userCreateInfoList); + this.saveUserAndRole(userBatchCreateDTO, source, sessionId); + } + + } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/utils/UserImportEventListener.java b/backend/services/system-setting/src/main/java/io/metersphere/system/utils/UserImportEventListener.java new file mode 100644 index 0000000000..a467c66588 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/utils/UserImportEventListener.java @@ -0,0 +1,66 @@ +package io.metersphere.system.utils; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import io.metersphere.sdk.dto.ExcelParseDTO; +import io.metersphere.sdk.util.BeanUtils; +import io.metersphere.sdk.util.LogUtils; +import io.metersphere.sdk.util.Translator; +import io.metersphere.system.dto.excel.ExcelValidateHelper; +import io.metersphere.system.dto.excel.UserExcel; +import io.metersphere.system.dto.excel.UserExcelRowDTO; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +public class UserImportEventListener extends AnalysisEventListener { + private ExcelParseDTO excelParseDTO; + + public UserImportEventListener() { + excelParseDTO = new ExcelParseDTO<>(); + } + + @Override + public void invoke(UserExcel data, AnalysisContext analysisContext) { + String errMsg; + Integer rowIndex = analysisContext.readRowHolder().getRowIndex(); + try { + //使用javax.validation校验excel数据 + errMsg = ExcelValidateHelper.validateEntity(data); + if (StringUtils.isEmpty(errMsg)) { + errMsg = businessValidate(data); + } + } catch (NoSuchFieldException e) { + errMsg = Translator.get("excel.parse.error"); + LogUtils.error(e.getMessage(), e); + } + UserExcelRowDTO userExcelRowDTO = new UserExcelRowDTO(); + BeanUtils.copyBean(userExcelRowDTO, data); + userExcelRowDTO.setDataIndex(rowIndex); + if (StringUtils.isEmpty(errMsg)) { + excelParseDTO.addRowData(userExcelRowDTO); + } else { + userExcelRowDTO.setErrorMessage(errMsg); + excelParseDTO.addErrorRowData(rowIndex, userExcelRowDTO); + } + } + + private String businessValidate(UserExcel rowData) { + if (CollectionUtils.isNotEmpty(excelParseDTO.getDataList())) { + for (UserExcelRowDTO userExcelRowDTO : excelParseDTO.getDataList()) { + if (StringUtils.equalsIgnoreCase(userExcelRowDTO.getEmail(), rowData.getEmail())) { + return Translator.get("user.email.repeat") + ":" + rowData.getEmail(); + } + } + } + return null; + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + + } + + public ExcelParseDTO getExcelParseDTO() { + return excelParseDTO; + } +} diff --git a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/UserControllerNonePermissionTests.java b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/UserControllerNonePermissionTests.java index 88c5109b18..fed593f7f3 100644 --- a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/UserControllerNonePermissionTests.java +++ b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/UserControllerNonePermissionTests.java @@ -4,8 +4,8 @@ import com.jayway.jsonpath.JsonPath; import io.metersphere.sdk.constants.SessionConstants; import io.metersphere.sdk.util.JSON; import io.metersphere.system.dto.UserCreateInfo; -import io.metersphere.system.dto.UserEditEnableRequest; import io.metersphere.system.dto.UserRoleOption; +import io.metersphere.system.dto.request.UserEditEnableRequest; import io.metersphere.system.utils.UserTestUtils; import jakarta.annotation.Resource; import org.apache.commons.lang3.StringUtils; diff --git a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/UserControllerTests.java b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/UserControllerTests.java index 36ac79d0f1..03748c4039 100644 --- a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/UserControllerTests.java +++ b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/UserControllerTests.java @@ -4,12 +4,20 @@ import base.BaseTest; import io.metersphere.sdk.constants.SessionConstants; import io.metersphere.sdk.controller.handler.ResultHolder; import io.metersphere.sdk.dto.BasePageRequest; +import io.metersphere.sdk.dto.ExcelParseDTO; import io.metersphere.sdk.dto.UserDTO; import io.metersphere.sdk.util.BeanUtils; import io.metersphere.sdk.util.JSON; import io.metersphere.sdk.util.Pager; -import io.metersphere.system.dto.*; +import io.metersphere.system.dto.UserBatchCreateDTO; +import io.metersphere.system.dto.UserCreateInfo; +import io.metersphere.system.dto.UserRoleOption; +import io.metersphere.system.dto.excel.UserExcelRowDTO; +import io.metersphere.system.dto.request.UserEditEnableRequest; +import io.metersphere.system.dto.request.UserEditRequest; +import io.metersphere.system.dto.response.UserImportResponse; import io.metersphere.system.dto.response.UserTableResponse; +import io.metersphere.system.service.UserService; import io.metersphere.system.utils.UserTestUtils; import io.metersphere.utils.JsonUtils; import jakarta.annotation.Resource; @@ -19,6 +27,7 @@ import org.junit.jupiter.api.*; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.SqlConfig; import org.springframework.test.web.servlet.MockMvc; @@ -44,6 +53,9 @@ public class UserControllerTests extends BaseTest { @Resource private MockMvc mockMvc; + @Resource + private UserService userService; + //失败请求返回编码 private static final ResultMatcher BAD_REQUEST_MATCHER = status().isBadRequest(); private static final ResultMatcher ERROR_REQUEST_MATCHER = status().is5xxServerError(); @@ -103,6 +115,17 @@ public class UserControllerTests extends BaseTest { .andReturn(); } + private MvcResult responseFile(String url, MockMultipartFile file) throws Exception { + return mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(file) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + .header(SessionConstants.HEADER_TOKEN, sessionId) + .header(SessionConstants.CSRF_TOKEN, csrfToken)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andReturn(); + } + private MvcResult responseGet(String url) throws Exception { return mockMvc.perform(MockMvcRequestBuilders.get(url) .header(SessionConstants.HEADER_TOKEN, sessionId) @@ -355,6 +378,10 @@ public class UserControllerTests extends BaseTest { BasePageRequest basePageRequest = new BasePageRequest(); basePageRequest.setPageSize(5); this.requestPost(UserTestUtils.URL_USER_PAGE, basePageRequest, BAD_REQUEST_MATCHER); + //pageSize超过100 + basePageRequest = UserTestUtils.getDefaultPageRequest(); + basePageRequest.setPageSize(250); + this.requestPost(UserTestUtils.URL_USER_PAGE, basePageRequest, BAD_REQUEST_MATCHER); //当前页数不大于5 basePageRequest = new BasePageRequest(); basePageRequest.setCurrent(1); @@ -489,4 +516,83 @@ public class UserControllerTests extends BaseTest { }}); this.requestPost(UserTestUtils.URL_USER_UPDATE_ENABLE, userChangeEnableRequest, ERROR_REQUEST_MATCHER); } + + + @Test + @Order(6) + public void testUserImportSuccess() throws Exception { + this.checkUserList(); + //测试用户数据导入。 每个导入文件都有10条数据,不同文件出错的数据不同。 + int importSuccessData = 10;//应该导入成功的数据数量 + int[] errorDataIndex = {};//出错数据的行数 + UserImportResponse response;//导入返回值 + //导入正常文件 + String filePath = this.getClass().getClassLoader().getResource("file/user_import_success.xlsx").getPath(); + MockMultipartFile file = new MockMultipartFile("file", "userImport.xlsx", MediaType.APPLICATION_OCTET_STREAM_VALUE, UserTestUtils.getFileBytes(filePath)); + ExcelParseDTO userImportReportDTOByFile = userService.getUserExcelParseDTO(file); + response = UserTestUtils.parseObjectFromMvcResult(this.responseFile(UserTestUtils.URL_USER_IMPORT, file), UserImportResponse.class); + UserTestUtils.checkImportResponse(response, importSuccessData, errorDataIndex);//检查返回值 + this.checkImportUserInDb(userImportReportDTOByFile);//检查数据已入库 + + + //导入空文件 + filePath = this.getClass().getClassLoader().getResource("file/user_import_success_empty.xlsx").getPath(); + file = new MockMultipartFile("file", "userImport.xlsx", MediaType.APPLICATION_OCTET_STREAM_VALUE, UserTestUtils.getFileBytes(filePath)); + response = UserTestUtils.parseObjectFromMvcResult(this.responseFile(UserTestUtils.URL_USER_IMPORT, file), UserImportResponse.class); + importSuccessData = 0; + errorDataIndex = new int[]{}; + UserTestUtils.checkImportResponse(response, importSuccessData, errorDataIndex); + + //文件内没有一条合格数据 + filePath = this.getClass().getClassLoader().getResource("file/user_import_error_all.xlsx").getPath(); + file = new MockMultipartFile("file", "userImport.xlsx", MediaType.APPLICATION_OCTET_STREAM_VALUE, UserTestUtils.getFileBytes(filePath)); + response = UserTestUtils.parseObjectFromMvcResult(this.responseFile(UserTestUtils.URL_USER_IMPORT, file), UserImportResponse.class); + importSuccessData = 0; + errorDataIndex = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + UserTestUtils.checkImportResponse(response, importSuccessData, errorDataIndex); + + //邮箱和数据库里的重复 + filePath = this.getClass().getClassLoader().getResource("file/user_import_error_email_repeat_db.xlsx").getPath(); + file = new MockMultipartFile("file", "userImport.xlsx", MediaType.APPLICATION_OCTET_STREAM_VALUE, UserTestUtils.getFileBytes(filePath)); + userImportReportDTOByFile = userService.getUserExcelParseDTO(file); + response = UserTestUtils.parseObjectFromMvcResult(this.responseFile(UserTestUtils.URL_USER_IMPORT, file), UserImportResponse.class); + importSuccessData = 8; + errorDataIndex = new int[]{1, 7}; + UserTestUtils.checkImportResponse(response, importSuccessData, errorDataIndex); + this.checkImportUserInDb(userImportReportDTOByFile);//检查数据已入库 + + //文件内邮箱重复 + filePath = this.getClass().getClassLoader().getResource("file/user_import_error_email_repeat_in_file.xlsx").getPath(); + file = new MockMultipartFile("file", "userImport.xlsx", MediaType.APPLICATION_OCTET_STREAM_VALUE, UserTestUtils.getFileBytes(filePath)); + userImportReportDTOByFile = userService.getUserExcelParseDTO(file); + response = UserTestUtils.parseObjectFromMvcResult(this.responseFile(UserTestUtils.URL_USER_IMPORT, file), UserImportResponse.class); + importSuccessData = 8; + errorDataIndex = new int[]{9, 10}; + UserTestUtils.checkImportResponse(response, importSuccessData, errorDataIndex); + this.checkImportUserInDb(userImportReportDTOByFile);//检查数据已入库 + + //文件不符合规范 + filePath = this.getClass().getClassLoader().getResource("file/abcde.gif").getPath(); + file = new MockMultipartFile("file", "userImport.xlsx", MediaType.APPLICATION_OCTET_STREAM_VALUE, UserTestUtils.getFileBytes(filePath)); + response = UserTestUtils.parseObjectFromMvcResult(this.responseFile(UserTestUtils.URL_USER_IMPORT, file), UserImportResponse.class); + importSuccessData = 0; + errorDataIndex = new int[]{}; + UserTestUtils.checkImportResponse(response, importSuccessData, errorDataIndex); + + //测试03版excel正常导入 + filePath = this.getClass().getClassLoader().getResource("file/user_import_success_03.xls").getPath(); + file = new MockMultipartFile("file", "userImport.xlsx", MediaType.APPLICATION_OCTET_STREAM_VALUE, UserTestUtils.getFileBytes(filePath)); + userImportReportDTOByFile = userService.getUserExcelParseDTO(file); + response = UserTestUtils.parseObjectFromMvcResult(this.responseFile(UserTestUtils.URL_USER_IMPORT, file), UserImportResponse.class); + importSuccessData = 10;//应该导入成功的数据数量 + errorDataIndex = new int[]{};//出错数据的行数 + UserTestUtils.checkImportResponse(response, importSuccessData, errorDataIndex);//检查返回值 + this.checkImportUserInDb(userImportReportDTOByFile);//检查数据已入库 + } + + public void checkImportUserInDb(ExcelParseDTO userImportReportDTOByFile) throws Exception { + for (UserExcelRowDTO item : userImportReportDTOByFile.getDataList()) { + Assertions.assertNotNull(this.getUserByEmail(item.getEmail())); + } + } } diff --git a/backend/services/system-setting/src/test/java/io/metersphere/system/utils/UserTestUtils.java b/backend/services/system-setting/src/test/java/io/metersphere/system/utils/UserTestUtils.java index 709d46efd2..ebc9599caa 100644 --- a/backend/services/system-setting/src/test/java/io/metersphere/system/utils/UserTestUtils.java +++ b/backend/services/system-setting/src/test/java/io/metersphere/system/utils/UserTestUtils.java @@ -8,14 +8,17 @@ import io.metersphere.sdk.util.JSON; import io.metersphere.system.domain.UserRole; import io.metersphere.system.dto.UserBatchCreateDTO; import io.metersphere.system.dto.UserCreateInfo; -import io.metersphere.system.dto.UserEditRequest; import io.metersphere.system.dto.UserRoleOption; +import io.metersphere.system.dto.request.UserEditRequest; +import io.metersphere.system.dto.response.UserImportResponse; import io.metersphere.utils.JsonUtils; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Assertions; import org.springframework.test.web.servlet.MvcResult; +import java.io.File; +import java.io.FileInputStream; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.Collectors; @@ -29,6 +32,7 @@ public class UserTestUtils { public static final String URL_USER_PAGE = "/system/user/page"; public static final String URL_GET_GLOBAL_SYSTEM = "/system/user/get/global/system/role"; public static final String URL_USER_UPDATE_ENABLE = "/system/user/update/enable"; + public static final String URL_USER_IMPORT = "/system/user/import"; public static T parseObjectFromMvcResult(MvcResult mvcResult, Class parseClass) { @@ -75,6 +79,30 @@ public class UserTestUtils { }}; } + public static byte[] getFileBytes(String filePath) { + File file = new File(filePath); + byte[] buffer = new byte[0]; + FileInputStream fi = null; + try { + fi = new FileInputStream(file); + buffer = new byte[(int) file.length()]; + int offset = 0; + int numRead = 0; + while (offset < buffer.length + && (numRead = fi.read(buffer, offset, buffer.length - offset)) >= 0) { + offset += numRead; + } + } catch (Exception ignore) { + } finally { + try { + fi.close(); + } catch (Exception ignore) { + } + + } + return buffer; + } + public static void compareUserDTO(UserEditRequest editRequest, UserDTO selectUserDTO) { Assertions.assertNotNull(editRequest); Assertions.assertNotNull(selectUserDTO); @@ -96,4 +124,17 @@ public class UserTestUtils { editRequest.getUserRoleIdList().containsAll(selectUserSystemRoleId) && selectUserSystemRoleId.containsAll(editRequest.getUserRoleIdList())); } + + public static void checkImportResponse(UserImportResponse responsePost, int successCount, int[] errorDataIndex) { + //导入总数据是否一致 + Assertions.assertTrue(responsePost.getImportCount() == successCount + errorDataIndex.length); + //导入成功数据是否一致 + Assertions.assertTrue(responsePost.getSuccessCount() == successCount); + //报错数据数量是否一致 + Assertions.assertTrue(responsePost.getErrorMessages().size() == errorDataIndex.length); + //报错数据行编码是否一致 + for (int index : errorDataIndex) { + Assertions.assertTrue(responsePost.getErrorMessages().containsKey(index)); + } + } } diff --git a/backend/services/system-setting/src/test/resources/file/abcde.gif b/backend/services/system-setting/src/test/resources/file/abcde.gif new file mode 100644 index 0000000000..b7efc929f2 Binary files /dev/null and b/backend/services/system-setting/src/test/resources/file/abcde.gif differ diff --git a/backend/services/system-setting/src/test/resources/file/user_import_error_all.xlsx b/backend/services/system-setting/src/test/resources/file/user_import_error_all.xlsx new file mode 100644 index 0000000000..e8d7f37f7a Binary files /dev/null and b/backend/services/system-setting/src/test/resources/file/user_import_error_all.xlsx differ diff --git a/backend/services/system-setting/src/test/resources/file/user_import_error_email_repeat_db.xlsx b/backend/services/system-setting/src/test/resources/file/user_import_error_email_repeat_db.xlsx new file mode 100644 index 0000000000..d05147ee39 Binary files /dev/null and b/backend/services/system-setting/src/test/resources/file/user_import_error_email_repeat_db.xlsx differ diff --git a/backend/services/system-setting/src/test/resources/file/user_import_error_email_repeat_in_file.xlsx b/backend/services/system-setting/src/test/resources/file/user_import_error_email_repeat_in_file.xlsx new file mode 100644 index 0000000000..20400b74b2 Binary files /dev/null and b/backend/services/system-setting/src/test/resources/file/user_import_error_email_repeat_in_file.xlsx differ diff --git a/backend/services/system-setting/src/test/resources/file/user_import_success.xlsx b/backend/services/system-setting/src/test/resources/file/user_import_success.xlsx new file mode 100644 index 0000000000..9e7fe14bae Binary files /dev/null and b/backend/services/system-setting/src/test/resources/file/user_import_success.xlsx differ diff --git a/backend/services/system-setting/src/test/resources/file/user_import_success_03.xls b/backend/services/system-setting/src/test/resources/file/user_import_success_03.xls new file mode 100644 index 0000000000..6c8f5127e3 Binary files /dev/null and b/backend/services/system-setting/src/test/resources/file/user_import_success_03.xls differ diff --git a/backend/services/system-setting/src/test/resources/file/user_import_success_empty.xlsx b/backend/services/system-setting/src/test/resources/file/user_import_success_empty.xlsx new file mode 100644 index 0000000000..8e1678dba2 Binary files /dev/null and b/backend/services/system-setting/src/test/resources/file/user_import_success_empty.xlsx differ