diff --git a/backend/services/project-management/pom.xml b/backend/services/project-management/pom.xml index 4c651ed7dd..e7dce9180e 100644 --- a/backend/services/project-management/pom.xml +++ b/backend/services/project-management/pom.xml @@ -18,6 +18,7 @@ metersphere-sdk ${revision} + io.metersphere metersphere-sdk @@ -26,6 +27,12 @@ test-jar test + + + io.metersphere + metersphere-system-setting + ${revision} + diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/controller/ProjectMemberController.java b/backend/services/project-management/src/main/java/io/metersphere/project/controller/ProjectMemberController.java new file mode 100644 index 0000000000..30df624023 --- /dev/null +++ b/backend/services/project-management/src/main/java/io/metersphere/project/controller/ProjectMemberController.java @@ -0,0 +1,101 @@ +package io.metersphere.project.controller; + +import com.github.pagehelper.Page; +import com.github.pagehelper.PageHelper; +import io.metersphere.project.dto.ProjectUserDTO; +import io.metersphere.project.request.ProjectMemberAddRequest; +import io.metersphere.project.request.ProjectMemberBatchDeleteRequest; +import io.metersphere.project.request.ProjectMemberEditRequest; +import io.metersphere.project.request.ProjectMemberRequest; +import io.metersphere.project.service.ProjectMemberService; +import io.metersphere.sdk.constants.PermissionConstants; +import io.metersphere.sdk.dto.OptionDTO; +import io.metersphere.sdk.util.PageUtils; +import io.metersphere.sdk.util.Pager; +import io.metersphere.sdk.util.SessionUtils; +import io.metersphere.system.dto.UserExtend; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +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 java.util.List; + +/** + * @author song-cc-rock + */ +@Tag(name = "项目管理-成员") +@RestController +@RequestMapping("/project/member") +public class ProjectMemberController { + + @Resource + private ProjectMemberService projectMemberService; + + @PostMapping("/list") + @Operation(summary = "项目管理-成员-列表查询") + @RequiresPermissions(PermissionConstants.PROJECT_MEMBER_READ) + public Pager> listMember(@Validated @RequestBody ProjectMemberRequest request) { + Page page = PageHelper.startPage(request.getCurrent(), request.getPageSize(), true); + return PageUtils.setPageInfo(page, projectMemberService.listMember(request)); + } + + @GetMapping("/get-member/option/{projectId}") + @Operation(summary = "项目管理-成员-获取成员下拉选项") + @RequiresPermissions(PermissionConstants.PROJECT_MEMBER_ADD) + public List getMemberOption(@PathVariable String projectId) { + return projectMemberService.getMemberOption(projectId); + } + + @GetMapping("/get-role/option/{projectId}") + @Operation(summary = "项目管理-成员-获取用户组下拉选项") + @RequiresPermissions(PermissionConstants.PROJECT_MEMBER_ADD) + public List getRoleOption(@PathVariable String projectId) { + return projectMemberService.getRoleOption(projectId); + } + + @PostMapping("/add") + @Operation(summary = "项目管理-成员-添加成员") + @RequiresPermissions(PermissionConstants.PROJECT_MEMBER_ADD) + public void addMember(@RequestBody ProjectMemberAddRequest request) { + projectMemberService.addMember(request, SessionUtils.getUserId()); + } + + @PostMapping("/update") + @Operation(summary = "项目管理-成员-编辑成员") + @RequiresPermissions(PermissionConstants.PROJECT_MEMBER_UPDATE) + public void updateMember(@RequestBody ProjectMemberEditRequest request) { + projectMemberService.updateMember(request, SessionUtils.getUserId()); + } + + @GetMapping("/remove/{projectId}/{userId}") + @Operation(summary = "项目管理-成员-移除成员") + @Parameters({ + @Parameter(name = "projectId", description = "项目ID", schema = @Schema(requiredMode = Schema.RequiredMode.REQUIRED)), + @Parameter(name = "userId", description = "成员ID", schema = @Schema(requiredMode = Schema.RequiredMode.REQUIRED)) + }) + @RequiresPermissions(PermissionConstants.PROJECT_MEMBER_DELETE) + public void removeMember(@PathVariable String projectId, @PathVariable String userId) { + projectMemberService.removeMember(projectId, userId); + } + + @PostMapping("/add-role") + @Operation(summary = "项目管理-成员-批量添加至用户组") + @RequiresPermissions(PermissionConstants.PROJECT_MEMBER_UPDATE) + public void addMemberRole(@RequestBody ProjectMemberAddRequest request) { + projectMemberService.addMemberRole(request, SessionUtils.getUserId()); + } + + @PostMapping("/batch/remove") + @Operation(summary = "项目管理-成员-批量从项目移除") + @RequiresPermissions(PermissionConstants.PROJECT_MEMBER_DELETE) + public void batchRemove(@RequestBody ProjectMemberBatchDeleteRequest request) { + projectMemberService.batchRemove(request); + } +} diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/dto/ProjectUserDTO.java b/backend/services/project-management/src/main/java/io/metersphere/project/dto/ProjectUserDTO.java new file mode 100644 index 0000000000..d92f18b907 --- /dev/null +++ b/backend/services/project-management/src/main/java/io/metersphere/project/dto/ProjectUserDTO.java @@ -0,0 +1,20 @@ +package io.metersphere.project.dto; + +import io.metersphere.system.domain.User; +import io.metersphere.system.domain.UserRole; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * @author song-cc-rock + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class ProjectUserDTO extends User { + + @Schema(description = "用户组集合") + private List userRoles; +} diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/mapper/ExtProjectMemberMapper.java b/backend/services/project-management/src/main/java/io/metersphere/project/mapper/ExtProjectMemberMapper.java new file mode 100644 index 0000000000..72962dcdca --- /dev/null +++ b/backend/services/project-management/src/main/java/io/metersphere/project/mapper/ExtProjectMemberMapper.java @@ -0,0 +1,20 @@ +package io.metersphere.project.mapper; + +import io.metersphere.project.request.ProjectMemberRequest; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @author song-cc-rock + */ +public interface ExtProjectMemberMapper { + + /** + * 获取项目成员列表 + * + * @param request 请求参数 + * @return 成员列表及用户组关联信息 + */ + List listMember(@Param("request") ProjectMemberRequest request); +} diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/mapper/ExtProjectMemberMapper.xml b/backend/services/project-management/src/main/java/io/metersphere/project/mapper/ExtProjectMemberMapper.xml new file mode 100644 index 0000000000..83bc64f43c --- /dev/null +++ b/backend/services/project-management/src/main/java/io/metersphere/project/mapper/ExtProjectMemberMapper.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/request/ProjectMemberAddRequest.java b/backend/services/project-management/src/main/java/io/metersphere/project/request/ProjectMemberAddRequest.java new file mode 100644 index 0000000000..4d08836214 --- /dev/null +++ b/backend/services/project-management/src/main/java/io/metersphere/project/request/ProjectMemberAddRequest.java @@ -0,0 +1,30 @@ +package io.metersphere.project.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.List; + +/** + * @author song-cc-rock + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class ProjectMemberAddRequest implements Serializable { + + @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{project.id.not_blank}") + private String projectId; + + @Schema(description = "用户ID集合", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "{user.id.not_blank}") + private List userIds; + + @Schema(description = "用户组ID集合", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "{user_role.id.not_blank}") + private List roleIds; +} diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/request/ProjectMemberBatchDeleteRequest.java b/backend/services/project-management/src/main/java/io/metersphere/project/request/ProjectMemberBatchDeleteRequest.java new file mode 100644 index 0000000000..4619eaf49a --- /dev/null +++ b/backend/services/project-management/src/main/java/io/metersphere/project/request/ProjectMemberBatchDeleteRequest.java @@ -0,0 +1,26 @@ +package io.metersphere.project.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.List; + +/** + * @author song-cc-rock + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class ProjectMemberBatchDeleteRequest implements Serializable { + + @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{project.id.not_blank}") + private String projectId; + + @Schema(description = "用户ID集合", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "{user.id.not_blank}") + private List userIds; +} diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/request/ProjectMemberEditRequest.java b/backend/services/project-management/src/main/java/io/metersphere/project/request/ProjectMemberEditRequest.java new file mode 100644 index 0000000000..d24b2118dd --- /dev/null +++ b/backend/services/project-management/src/main/java/io/metersphere/project/request/ProjectMemberEditRequest.java @@ -0,0 +1,30 @@ +package io.metersphere.project.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.util.List; + +/** + * @author song-cc-rock + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class ProjectMemberEditRequest implements Serializable { + + @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{project.id.not_blank}") + private String projectId; + + @Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "{user.id.not_blank}") + private String userId; + + @Schema(description = "用户组ID集合", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "{user_role.id.not_blank}") + private List roleIds; +} diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/request/ProjectMemberRequest.java b/backend/services/project-management/src/main/java/io/metersphere/project/request/ProjectMemberRequest.java new file mode 100644 index 0000000000..6aca001912 --- /dev/null +++ b/backend/services/project-management/src/main/java/io/metersphere/project/request/ProjectMemberRequest.java @@ -0,0 +1,16 @@ +package io.metersphere.project.request; + +import io.metersphere.sdk.dto.BasePageRequest; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = false) +public class ProjectMemberRequest extends BasePageRequest { + + @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "{project.id.not_blank}") + private String projectId; +} diff --git a/backend/services/project-management/src/main/java/io/metersphere/project/service/ProjectMemberService.java b/backend/services/project-management/src/main/java/io/metersphere/project/service/ProjectMemberService.java new file mode 100644 index 0000000000..5cf65d66c2 --- /dev/null +++ b/backend/services/project-management/src/main/java/io/metersphere/project/service/ProjectMemberService.java @@ -0,0 +1,248 @@ +package io.metersphere.project.service; + +import io.metersphere.project.domain.Project; +import io.metersphere.project.dto.ProjectUserDTO; +import io.metersphere.project.mapper.ExtProjectMemberMapper; +import io.metersphere.project.mapper.ProjectMapper; +import io.metersphere.project.request.ProjectMemberAddRequest; +import io.metersphere.project.request.ProjectMemberBatchDeleteRequest; +import io.metersphere.project.request.ProjectMemberEditRequest; +import io.metersphere.project.request.ProjectMemberRequest; +import io.metersphere.sdk.constants.UserRoleEnum; +import io.metersphere.sdk.constants.UserRoleType; +import io.metersphere.sdk.dto.OptionDTO; +import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.mapper.BaseUserMapper; +import io.metersphere.sdk.util.BeanUtils; +import io.metersphere.sdk.util.Translator; +import io.metersphere.system.domain.*; +import io.metersphere.system.dto.UserExtend; +import io.metersphere.system.mapper.ExtOrganizationMapper; +import io.metersphere.system.mapper.UserMapper; +import io.metersphere.system.mapper.UserRoleMapper; +import io.metersphere.system.mapper.UserRoleRelationMapper; +import io.metersphere.system.request.OrganizationRequest; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author song-cc-rock + */ +@Service +@Transactional +public class ProjectMemberService { + + @Resource + private UserRoleMapper userRoleMapper; + @Resource + private UserMapper userMapper; + @Resource + private UserRoleRelationMapper userRoleRelationMapper; + @Resource + private BaseUserMapper baseUserMapper; + @Resource + private ProjectMapper projectMapper; + @Resource + private ExtOrganizationMapper extOrganizationMapper; + @Resource + private ExtProjectMemberMapper extProjectMemberMapper; + + /** + * 获取成员列表 + * + * @param request 请求参数 + * @return 成员列表 + */ + public List listMember(ProjectMemberRequest request) { + // 查询当前项目成员 + List members = extProjectMemberMapper.listMember(request); + UserRoleRelationExample relationExample = new UserRoleRelationExample(); + relationExample.createCriteria().andSourceIdEqualTo(request.getProjectId()).andUserIdIn(members); + List userRoleRelates = userRoleRelationMapper.selectByExample(relationExample); + Map> userRoleRelateMap = userRoleRelates.stream().collect(Collectors.groupingBy(UserRoleRelation::getUserId, + Collectors.mapping(UserRoleRelation::getRoleId, Collectors.toList()))); + // 查询所有用户 + List users = baseUserMapper.findAll(); + Map userMap = users.stream().collect(Collectors.toMap(User::getId, user -> user)); + // 查询所有项目类型用户组 + UserRoleExample example = new UserRoleExample(); + example.createCriteria().andTypeEqualTo(UserRoleType.PROJECT.name()); + List roles = userRoleMapper.selectByExample(example); + Map roleMap = roles.stream().collect(Collectors.toMap(UserRole::getId, role -> role)); + List projectUsers = new ArrayList<>(); + userRoleRelateMap.forEach((k, v) -> { + ProjectUserDTO projectUser = new ProjectUserDTO(); + User user = userMap.get(k); + BeanUtils.copyBean(projectUser, user); + List userRoles = new ArrayList<>(); + v.forEach(roleId -> { + UserRole role = roleMap.get(roleId); + userRoles.add(role); + }); + projectUser.setUserRoles(userRoles); + projectUsers.add(projectUser); + }); + return projectUsers; + } + + /** + * 获取组织成员下拉选项 + * + * @param projectId 项目ID + * @return 项目成员下拉选项 + */ + public List getMemberOption(String projectId) { + Project project = projectMapper.selectByPrimaryKey(projectId); + if (project == null) { + return new ArrayList<>(); + } + return extOrganizationMapper.getMemberByOrg(project.getOrganizationId()); + } + + /** + * 获取项目用户组下拉选项 + * + * @param projectId 项目ID + * @return 用户组下拉选项 + */ + public List getRoleOption(String projectId) { + UserRoleExample example = new UserRoleExample(); + example.createCriteria().andTypeEqualTo(UserRoleType.PROJECT.name()) + .andScopeIdIn(Arrays.asList(projectId, UserRoleEnum.GLOBAL.toString())); + List userRoles = userRoleMapper.selectByExample(example); + return userRoles.stream().map(userRole -> new OptionDTO(userRole.getId(), userRole.getName())).toList(); + } + + /** + * 添加成员(项目) + * + * @param request 请求参数 + * @param currentUserId 当前用户ID + */ + public void addMember(ProjectMemberAddRequest request, String currentUserId) { + // 项目不存在, 则不添加 + checkProjectExist(request.getProjectId()); + // 获取已经存在的用户组 + UserRoleRelationExample example = new UserRoleRelationExample(); + example.createCriteria().andSourceIdEqualTo(request.getProjectId()) + .andUserIdIn(request.getUserIds()).andRoleIdIn(request.getRoleIds()); + List userRoleRelations = userRoleRelationMapper.selectByExample(example); + Map> existUserRelations = userRoleRelations.stream().collect( + Collectors.groupingBy(UserRoleRelation::getUserId, Collectors.mapping(UserRoleRelation::getRoleId, Collectors.toList()))); + // 比较用户组是否已经存在, 如果不存在则添加 + List relations = new ArrayList<>(); + request.getUserIds().forEach(userId -> request.getRoleIds().forEach(roleId -> { + // 用户不存在或用户组不存在, 则不添加 + if (isUserOrRoleNotExist(userId, roleId)) { + return; + } + // 如果该用户已经添加至该用户组, 则不再添加 + if (existUserRelations.containsKey(userId) && existUserRelations.get(userId).contains(roleId)) { + return; + } + UserRoleRelation relation = new UserRoleRelation(); + relation.setId(UUID.randomUUID().toString()); + relation.setUserId(userId); + relation.setRoleId(roleId); + relation.setSourceId(request.getProjectId()); + relation.setCreateTime(System.currentTimeMillis()); + relation.setCreateUser(currentUserId); + relations.add(relation); + })); + if (!CollectionUtils.isEmpty(relations)) { + userRoleRelationMapper.batchInsert(relations); + } + } + + /** + * 更新成员(项目) + * + * @param request 请求参数 + * @param currentUserId 当前用户ID + */ + public void updateMember(ProjectMemberEditRequest request, String currentUserId) { + // 项目不存在 + checkProjectExist(request.getProjectId()); + // 移除已经存在的用户组 + UserRoleRelationExample example = new UserRoleRelationExample(); + example.createCriteria().andSourceIdEqualTo(request.getProjectId()) + .andUserIdEqualTo(request.getUserId()); + userRoleRelationMapper.deleteByExample(example); + // 添加新的用户组 + List relations = new ArrayList<>(); + request.getRoleIds().forEach(roleId -> { + // 用户不存在或用户组不存在, 则不添加 + if (isUserOrRoleNotExist(request.getUserId(), roleId)) { + return; + } + UserRoleRelation relation = new UserRoleRelation(); + relation.setId(UUID.randomUUID().toString()); + relation.setUserId(request.getUserId()); + relation.setRoleId(roleId); + relation.setSourceId(request.getProjectId()); + relation.setCreateTime(System.currentTimeMillis()); + relation.setCreateUser(currentUserId); + relations.add(relation); + }); + if (!CollectionUtils.isEmpty(relations)) { + userRoleRelationMapper.batchInsert(relations); + } + } + + /** + * 移除成员(项目) + * + * @param projectId 项目ID + * @param userId 用户ID + */ + public void removeMember(String projectId, String userId) { + // 项目不存在, 则不移除 + checkProjectExist(projectId); + // 移除成员, 则移除该成员在该项目下的所有用户组 + UserRoleRelationExample example = new UserRoleRelationExample(); + example.createCriteria().andSourceIdEqualTo(projectId).andUserIdEqualTo(userId); + userRoleRelationMapper.deleteByExample(example); + } + + /** + * 批量添加成员至用户组(项目) + * @param request 请求参数 + * @param currentUserId 当前用户ID + */ + public void addMemberRole(ProjectMemberAddRequest request, String currentUserId) { + // 添加用户用户组(已经添加的用户组不再添加) + addMember(request, currentUserId); + } + + /** + * 批量移除成员(项目) + * @param request 请求参数 + */ + public void batchRemove(ProjectMemberBatchDeleteRequest request) { + // 项目不存在, 则不移除 + checkProjectExist(request.getProjectId()); + // 批量移除成员, 则移除该成员在该项目下的所有用户组 + UserRoleRelationExample example = new UserRoleRelationExample(); + example.createCriteria().andSourceIdEqualTo(request.getProjectId()) + .andUserIdIn(request.getUserIds()); + userRoleRelationMapper.deleteByExample(example); + } + + private void checkProjectExist(String projectId) { + Project project = projectMapper.selectByPrimaryKey(projectId); + if (project == null) { + throw new MSException(Translator.get("project_not_exist")); + } + } + + private boolean isUserOrRoleNotExist(String userId, String roleId) { + UserExample example = new UserExample(); + example.createCriteria().andIdEqualTo(userId).andDeletedEqualTo(false); + return userMapper.selectByExample(example) == null || userRoleMapper.selectByPrimaryKey(roleId) == null; + } +} diff --git a/backend/services/project-management/src/test/java/io/metersphere/project/controller/ProjectMemberControllerTests.java b/backend/services/project-management/src/test/java/io/metersphere/project/controller/ProjectMemberControllerTests.java new file mode 100644 index 0000000000..32ec16e984 --- /dev/null +++ b/backend/services/project-management/src/test/java/io/metersphere/project/controller/ProjectMemberControllerTests.java @@ -0,0 +1,235 @@ +package io.metersphere.project.controller; + +import io.metersphere.project.dto.ProjectUserDTO; +import io.metersphere.project.request.ProjectMemberAddRequest; +import io.metersphere.project.request.ProjectMemberBatchDeleteRequest; +import io.metersphere.project.request.ProjectMemberEditRequest; +import io.metersphere.project.request.ProjectMemberRequest; +import io.metersphere.sdk.base.BaseTest; +import io.metersphere.sdk.constants.SessionConstants; +import io.metersphere.sdk.controller.handler.ResultHolder; +import io.metersphere.sdk.util.JSON; +import io.metersphere.sdk.util.Pager; +import org.apache.commons.lang3.StringUtils; +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.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ProjectMemberControllerTests extends BaseTest { + + public static final String LIST_MEMBER = "/project/member/list"; + public static final String GET_MEMBER = "/project/member/get-member/option"; + public static final String GET_ROLE = "/project/member/get-role/option"; + public static final String ADD_MEMBER = "/project/member/add"; + public static final String UPDATE_MEMBER = "/project/member/update"; + public static final String REMOVE_MEMBER = "/project/member/remove"; + public static final String ADD_ROLE = "/project/member/add-role"; + public static final String BATCH_REMOVE_MEMBER = "/project/member/batch/remove"; + + @Test + @Order(1) + @Sql(scripts = {"/dml/init_project_member.sql"}, config = @SqlConfig(encoding = "utf-8", transactionMode = SqlConfig.TransactionMode.ISOLATED)) + public void testListMemberSuccess() throws Exception{ + ProjectMemberRequest request = new ProjectMemberRequest(); + request.setProjectId("default-project-member-test"); + request.setCurrent(1); + request.setPageSize(10); + request.setKeyword("default"); + MvcResult mvcResult = this.responsePost(LIST_MEMBER, request); + // 获取返回值 + String returnData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + ResultHolder resultHolder = JSON.parseObject(returnData, ResultHolder.class); + // 返回请求正常 + Assertions.assertNotNull(resultHolder); + Pager pageData = JSON.parseObject(JSON.toJSONString(resultHolder.getData()), Pager.class); + // 返回值不为空 + Assertions.assertNotNull(pageData); + // 返回值的页码和当前页码相同 + Assertions.assertEquals(pageData.getCurrent(), request.getCurrent()); + // 返回的数据量不超过规定要返回的数据量相同 + Assertions.assertTrue(JSON.parseArray(JSON.toJSONString(pageData.getList())).size() <= request.getPageSize()); + // 返回值中取出第一条数据, 并判断name, email, phone是否包含关键字default + ProjectUserDTO projectUserDTO = JSON.parseArray(JSON.toJSONString(pageData.getList()), ProjectUserDTO.class).get(0); + Assertions.assertTrue(StringUtils.contains(projectUserDTO.getName(), request.getKeyword()) + || StringUtils.contains(projectUserDTO.getEmail(), request.getKeyword()) + || StringUtils.contains(projectUserDTO.getPhone(), request.getKeyword())); + } + + @Test + @Order(2) + public void testListMemberError() throws Exception { + // 页码有误 + ProjectMemberRequest request = new ProjectMemberRequest(); + request.setCurrent(0); + request.setPageSize(10); + this.requestPost(LIST_MEMBER, request, status().isBadRequest()); + // 页数有误 + request = new ProjectMemberRequest(); + request.setCurrent(1); + request.setPageSize(1); + this.requestPost(LIST_MEMBER, request, status().isBadRequest()); + } + + @Test + @Order(3) + public void testGetMemberOption() throws Exception { + this.requestGet(GET_MEMBER + "/default-project-member-test", status().isOk()); + // 覆盖空数据 + this.requestGet(GET_MEMBER + "/default-project-member-x", status().isOk()); + } + + @Test + @Order(4) + public void testGetRoleOption() throws Exception { + this.requestGet(GET_ROLE + "/default-project-member-test", status().isOk()); + } + + @Test + @Order(5) + public void testAddMemberSuccess() throws Exception { + ProjectMemberAddRequest request = new ProjectMemberAddRequest(); + request.setProjectId("default-project-member-test"); + request.setUserIds(List.of("default-project-member-user-1", "default-project-member-user-del")); + request.setRoleIds(List.of("project_admin", "project_admin_x", "project_member")); + this.requestPost(ADD_MEMBER, request, status().isOk()); + } + + @Test + @Order(6) + public void testAddMemberRepeat() throws Exception { + ProjectMemberAddRequest request = new ProjectMemberAddRequest(); + request.setProjectId("default-project-member-test"); + request.setUserIds(List.of("default-project-member-user-1")); + request.setRoleIds(List.of("project_admin")); + this.requestPost(ADD_MEMBER, request, status().isOk()); + } + + @Test + @Order(7) + public void testAddMemberError() throws Exception { + ProjectMemberAddRequest request = new ProjectMemberAddRequest(); + request.setProjectId("default-project-member-x"); + request.setUserIds(List.of("default-project-member-user-1", "default-project-member-user-del")); + request.setRoleIds(List.of("project_admin", "project_admin_x", "project_member")); + this.requestPost(ADD_MEMBER, request, status().is5xxServerError()); + } + + @Test + @Order(8) + public void testUpdateMemberSuccess() throws Exception { + // 不存在的用户组 + ProjectMemberEditRequest request = new ProjectMemberEditRequest(); + request.setProjectId("default-project-member-test"); + request.setUserId("default-project-member-user-1"); + request.setRoleIds(List.of("project_admin_x")); + this.requestPost(UPDATE_MEMBER, request, status().isOk()); + // 存在的用户组 + request.setRoleIds(List.of("project_admin", "project_member")); + this.requestPost(UPDATE_MEMBER, request, status().isOk()); + } + + @Test + @Order(9) + public void testUpdateMemberError() throws Exception { + ProjectMemberEditRequest request = new ProjectMemberEditRequest(); + request.setProjectId("default-project-member-x"); + request.setUserId("default-project-member-user-1"); + request.setRoleIds(List.of("project_admin", "project_admin_x", "project_member")); + this.requestPost(UPDATE_MEMBER, request, status().is5xxServerError()); + } + + @Test + @Order(10) + public void testRemoveMemberSuccess() throws Exception { + this.requestGet(REMOVE_MEMBER + "/default-project-member-test/default-project-member-user-1", status().isOk()); + } + + @Test + @Order(11) + public void testRemoveMemberError() throws Exception { + this.requestGet(REMOVE_MEMBER + "/default-project-member-x/default-project-member-user-1", status().is5xxServerError()); + } + + @Test + @Order(12) + public void testAddMemberRoleSuccess() throws Exception { + ProjectMemberAddRequest request = new ProjectMemberAddRequest(); + request.setProjectId("default-project-member-test"); + request.setUserIds(List.of("default-project-member-user-1", "default-project-member-user-2")); + request.setRoleIds(List.of("project_admin", "project_member")); + this.requestPost(ADD_ROLE, request, status().isOk()); + } + + @Test + @Order(13) + public void testAddMemberRoleError() throws Exception { + ProjectMemberAddRequest request = new ProjectMemberAddRequest(); + request.setProjectId("default-project-member-x"); + request.setUserIds(List.of("default-project-member-user-1", "default-project-member-user-2")); + request.setRoleIds(List.of("project_admin", "project_member")); + this.requestPost(ADD_ROLE, request, status().is5xxServerError()); + } + + @Test + @Order(14) + public void testBatchRemoveMemberSuccess() throws Exception { + ProjectMemberBatchDeleteRequest request = new ProjectMemberBatchDeleteRequest(); + request.setProjectId("default-project-member-test"); + request.setUserIds(List.of("default-project-member-user-1", "default-project-member-user-2")); + this.requestPost(BATCH_REMOVE_MEMBER, request, status().isOk()); + } + + @Test + @Order(15) + public void testBatchRemoveMember() throws Exception { + ProjectMemberBatchDeleteRequest request = new ProjectMemberBatchDeleteRequest(); + request.setProjectId("default-project-member-x"); + request.setUserIds(List.of("default-project-member-user-1", "default-project-member-user-2")); + this.requestPost(BATCH_REMOVE_MEMBER, request, status().is5xxServerError()); + } + + private void requestPost(String url, Object param, ResultMatcher resultMatcher) throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post(url) + .header(SessionConstants.HEADER_TOKEN, sessionId) + .header(SessionConstants.CSRF_TOKEN, csrfToken) + .content(JSON.toJSONString(param)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(resultMatcher) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); + } + + private MvcResult responsePost(String url, Object param) throws Exception { + return mockMvc.perform(MockMvcRequestBuilders.post(url) + .header(SessionConstants.HEADER_TOKEN, sessionId) + .header(SessionConstants.CSRF_TOKEN, csrfToken) + .content(JSON.toJSONString(param)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andReturn(); + } + + private void requestGet(String url, ResultMatcher resultMatcher) throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get(url) + .header(SessionConstants.HEADER_TOKEN, sessionId) + .header(SessionConstants.CSRF_TOKEN, csrfToken) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(resultMatcher) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); + } +} diff --git a/backend/services/project-management/src/test/resources/dml/init_project_member.sql b/backend/services/project-management/src/test/resources/dml/init_project_member.sql new file mode 100644 index 0000000000..a5cafe06e3 --- /dev/null +++ b/backend/services/project-management/src/test/resources/dml/init_project_member.sql @@ -0,0 +1,25 @@ +# 1. 准备组织, 项目, 用户数据 +# 2. 准备组织成员数据 +# 3. 准备项目成员数据 +INSERT INTO organization(id, num, name, description, create_time, update_time, create_user, update_user, deleted, delete_user, delete_time) VALUE + ('default-organization-member-test', null, 'efault-organization-member-test', 'efault-organization-member-test', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 'admin', 'admin', 0, null, null); +INSERT INTO project (id, num, organization_id, name, description, create_user, update_user, create_time, update_time) VALUE + ('default-project-member-test', null, 'efault-organization-member-test', '默认项目', '系统默认创建的项目', 'admin', 'admin', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000); +INSERT INTO user(id, name, email, password, create_time, update_time, language, last_organization_id, phone, source, last_project_id, create_user, update_user, deleted) VALUES + ('default-project-member-user-1', 'default-project-member-user1', 'project-member1@metersphere.io', MD5('metersphere'), UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NUll, '', 'LOCAL', NULL, 'admin', 'admin', 0), + ('default-project-member-user-2', 'default-project-member-user2', 'project-member2@metersphere.io', MD5('metersphere'), UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NUll, '', 'LOCAL', NULL, 'admin', 'admin', 0), + ('default-project-member-user-del', 'default-project-member-userDel', 'project-member-del@metersphere.io', MD5('metersphere'), UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NUll, '', 'LOCAL', NULL, 'admin', 'admin', 1); + +INSERT INTO user_role_relation (id, user_id, role_id, source_id, create_time, create_user) VALUES + (UUID(), 'default-project-member-user-1', 'org_member', 'default-organization-member-test', UNIX_TIMESTAMP() * 1000, 'admin'), + (UUID(), 'default-project-member-user-2', 'org_member', 'default-organization-member-test', UNIX_TIMESTAMP() * 1000, 'admin'); + +INSERT INTO user_role_relation (id, user_id, role_id, source_id, create_time, create_user) VALUES + (UUID(), 'default-project-member-user-1', 'project_admin', 'default-project-member-test', UNIX_TIMESTAMP() * 1000, 'admin'), + (UUID(), 'default-project-member-user-2', 'project_admin', 'default-project-member-test', UNIX_TIMESTAMP() * 1000, 'admin'), + (UUID(), 'default-project-member-user-del', 'project_admin', 'default-project-member-test', UNIX_TIMESTAMP() * 1000, 'admin'); + + + + + diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.java b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.java index 333ed2638d..7607feae0f 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.java @@ -77,5 +77,17 @@ public interface ExtOrganizationMapper { */ List selectOrganizationOptions(); + /** + * 获取组织下拉选项 + * @param ids 组织ID集合 + * @return 组织下拉选项 + */ List getOptionsByIds(@Param("ids") List ids); + + /** + * 获取所有组织成员 + * @param organizationId 组织ID + * @return 成员 + */ + List getMemberByOrg(String organizationId); } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.xml b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.xml index 5f4360bb82..eabd2d47f4 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.xml +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.xml @@ -80,6 +80,7 @@ + + +