diff --git a/pom.xml b/pom.xml index 30b2cca..6a66176 100644 --- a/pom.xml +++ b/pom.xml @@ -45,6 +45,13 @@ test + + + org.springframework.security + spring-security-test + test + + org.springframework.boot diff --git a/src/main/java/com/gxwebsoft/common/core/aspect/OperationLogAspect.java b/src/main/java/com/gxwebsoft/common/core/aspect/OperationLogAspect.java index fefe1c7..86e6cfb 100644 --- a/src/main/java/com/gxwebsoft/common/core/aspect/OperationLogAspect.java +++ b/src/main/java/com/gxwebsoft/common/core/aspect/OperationLogAspect.java @@ -89,9 +89,12 @@ public class OperationLogAspect { record.setUrl(request.getRequestURI()); record.setRequestMethod(request.getMethod()); UserAgent ua = UserAgentUtil.parse(ServletUtil.getHeaderIgnoreCase(request, "User-Agent")); - record.setOs(ua.getPlatform().toString()); - record.setDevice(ua.getOs().toString()); - record.setBrowser(ua.getBrowser().toString()); + // 非浏览器客户端可能不带 User-Agent,此时不解析终端信息 + if (ua != null) { + record.setOs(ua.getPlatform() == null ? null : ua.getPlatform().toString()); + record.setDevice(ua.getOs() == null ? null : ua.getOs().toString()); + record.setBrowser(ua.getBrowser() == null ? null : ua.getBrowser().toString()); + } record.setIp(ServletUtil.getClientIP(request)); } // 记录异常信息 diff --git a/src/main/java/com/gxwebsoft/common/system/controller/RoleMemberController.java b/src/main/java/com/gxwebsoft/common/system/controller/RoleMemberController.java new file mode 100644 index 0000000..8985d1d --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/controller/RoleMemberController.java @@ -0,0 +1,60 @@ +package com.gxwebsoft.common.system.controller; + +import com.gxwebsoft.common.core.annotation.OperationLog; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.common.system.param.RoleMemberParam; +import com.gxwebsoft.common.system.service.RoleMemberService; +import com.gxwebsoft.common.system.vo.RoleMemberVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +/** + * 角色成员控制器 + * + * @author WebSoft + * @since 2026-10-30 + */ +@Api(tags = "角色成员") +@RestController +@RequestMapping("/api/system/role-member") +public class RoleMemberController extends BaseController { + @Resource + private RoleMemberService roleMemberService; + + @PreAuthorize("hasAuthority('sys:role:update')") + @ApiOperation("分页查询角色成员") + @GetMapping("/page") + public ApiResult> page(RoleMemberParam param) { + return success(roleMemberService.pageMembers(param)); + } + + @PreAuthorize("hasAuthority('sys:role:update')") + @OperationLog + @ApiOperation("添加角色成员") + @PostMapping() + public ApiResult save(@RequestBody RoleMemberParam param) { + roleMemberService.addMember(param.getRoleId(), param.getUserId()); + return success("添加成功"); + } + + @PreAuthorize("hasAuthority('sys:role:update')") + @OperationLog + @ApiOperation("移除角色成员") + @DeleteMapping() + public ApiResult remove(@RequestParam Integer roleId, @RequestParam Integer userId) { + roleMemberService.removeMember(roleId, userId); + return success("移除成功"); + } +} diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/RoleMemberMapper.java b/src/main/java/com/gxwebsoft/common/system/mapper/RoleMemberMapper.java new file mode 100644 index 0000000..2bbc828 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/mapper/RoleMemberMapper.java @@ -0,0 +1,29 @@ +package com.gxwebsoft.common.system.mapper; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.gxwebsoft.common.system.vo.RoleMemberVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 角色成员Mapper + * + * @author WebSoft + * @since 2026-10-30 + */ +public interface RoleMemberMapper { + + /** + * 分页查询角色下的启用用户 + * + * @param page 分页对象 + * @param roleId 角色id + * @param keywords 账号/姓名关键词 + * @return List + */ + List selectPageByRoleId(@Param("page") IPage page, + @Param("roleId") Integer roleId, + @Param("keywords") String keywords); + +} diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/xml/RoleMemberMapper.xml b/src/main/java/com/gxwebsoft/common/system/mapper/xml/RoleMemberMapper.xml new file mode 100644 index 0000000..103cacf --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/mapper/xml/RoleMemberMapper.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/src/main/java/com/gxwebsoft/common/system/param/RoleMemberParam.java b/src/main/java/com/gxwebsoft/common/system/param/RoleMemberParam.java new file mode 100644 index 0000000..4f0d757 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/param/RoleMemberParam.java @@ -0,0 +1,31 @@ +package com.gxwebsoft.common.system.param; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.gxwebsoft.common.core.web.BaseParam; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 角色成员查询/操作参数 + * + * @author WebSoft + * @since 2026-10-30 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@ApiModel(description = "角色成员参数") +public class RoleMemberParam extends BaseParam { + private static final long serialVersionUID = 1L; + + @ApiModelProperty("角色id") + @TableField(exist = false) + private Integer roleId; + + @ApiModelProperty("用户id") + @TableField(exist = false) + private Integer userId; +} diff --git a/src/main/java/com/gxwebsoft/common/system/service/RoleMemberService.java b/src/main/java/com/gxwebsoft/common/system/service/RoleMemberService.java new file mode 100644 index 0000000..166b0bc --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/service/RoleMemberService.java @@ -0,0 +1,38 @@ +package com.gxwebsoft.common.system.service; + +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.common.system.param.RoleMemberParam; +import com.gxwebsoft.common.system.vo.RoleMemberVO; + +/** + * 角色成员Service + * + * @author WebSoft + * @since 2026-10-30 + */ +public interface RoleMemberService { + + /** + * 分页查询角色下的启用用户 + * + * @param param 查询参数 + * @return PageResult + */ + PageResult pageMembers(RoleMemberParam param); + + /** + * 添加角色成员 + * + * @param roleId 角色id + * @param userId 用户id + */ + void addMember(Integer roleId, Integer userId); + + /** + * 移除角色成员 + * + * @param roleId 角色id + * @param userId 用户id + */ + void removeMember(Integer roleId, Integer userId); +} diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/OperationRecordServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/OperationRecordServiceImpl.java index 8095bf4..00884df 100644 --- a/src/main/java/com/gxwebsoft/common/system/service/impl/OperationRecordServiceImpl.java +++ b/src/main/java/com/gxwebsoft/common/system/service/impl/OperationRecordServiceImpl.java @@ -1,5 +1,6 @@ package com.gxwebsoft.common.system.service.impl; +import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.common.core.web.PageResult; @@ -19,6 +20,7 @@ import java.util.List; * @since 2018-12-24 16:10:02 */ @Service +@DS("server") public class OperationRecordServiceImpl extends ServiceImpl implements OperationRecordService { diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/RoleMemberServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/RoleMemberServiceImpl.java new file mode 100644 index 0000000..190bcab --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/service/impl/RoleMemberServiceImpl.java @@ -0,0 +1,102 @@ +package com.gxwebsoft.common.system.service.impl; + +import com.baomidou.dynamic.datasource.annotation.DS; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.gxwebsoft.common.core.exception.BusinessException; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.common.system.entity.Role; +import com.gxwebsoft.common.system.entity.User; +import com.gxwebsoft.common.system.entity.UserRole; +import com.gxwebsoft.common.system.mapper.RoleMapper; +import com.gxwebsoft.common.system.mapper.RoleMemberMapper; +import com.gxwebsoft.common.system.mapper.UserMapper; +import com.gxwebsoft.common.system.mapper.UserRoleMapper; +import com.gxwebsoft.common.system.param.RoleMemberParam; +import com.gxwebsoft.common.system.service.RoleMemberService; +import com.gxwebsoft.common.system.vo.RoleMemberVO; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Collections; +import java.util.List; + +/** + * 角色成员Service实现 + * + * @author WebSoft + * @since 2026-10-30 + */ +@Service +@DS("server") +public class RoleMemberServiceImpl implements RoleMemberService { + @Resource + private RoleMapper roleMapper; + @Resource + private UserMapper userMapper; + @Resource + private UserRoleMapper userRoleMapper; + @Resource + private RoleMemberMapper roleMemberMapper; + + @Override + public PageResult pageMembers(RoleMemberParam param) { + PageParam page = new PageParam<>(param); + List list = roleMemberMapper.selectPageByRoleId(page, + param.getRoleId(), param.getKeywords()); + return new PageResult<>(list, page.getTotal()); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addMember(Integer roleId, Integer userId) { + requireIds(roleId, userId); + checkRoleExists(roleId); + User user = userMapper.selectById(userId); + if (user == null) { + throw new BusinessException("用户不存在"); + } + if (user.getStatus() == null || user.getStatus() != 0) { + throw new BusinessException("该用户已停用,不能添加为角色成员"); + } + if (userRoleMapper.selectCount(new QueryWrapper() + .eq("user_id", userId) + .eq("role_id", roleId)) > 0) { + throw new BusinessException("该用户已是角色成员"); + } + if (userRoleMapper.insertBatch(userId, Collections.singletonList(roleId)) < 1) { + throw new BusinessException("添加成员失败"); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void removeMember(Integer roleId, Integer userId) { + requireIds(roleId, userId); + checkRoleExists(roleId); + int removed = userRoleMapper.delete(new LambdaQueryWrapper() + .eq(UserRole::getUserId, userId) + .eq(UserRole::getRoleId, roleId)); + if (removed < 1) { + throw new BusinessException("该用户不是该角色成员"); + } + } + + private void requireIds(Integer roleId, Integer userId) { + if (roleId == null) { + throw new BusinessException("角色不能为空"); + } + if (userId == null) { + throw new BusinessException("用户不能为空"); + } + } + + private void checkRoleExists(Integer roleId) { + Role role = roleMapper.selectById(roleId); + if (role == null) { + throw new BusinessException("角色不存在"); + } + } +} diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java index 660f618..ccaa7d2 100644 --- a/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java @@ -49,6 +49,7 @@ public class UserServiceImpl extends ServiceImpl implements Us @Resource private RedisUtil redisUtil; + @DS("server") @Override public PageResult pageRel(UserParam param) { PageParam page = new PageParam<>(param); @@ -142,6 +143,7 @@ public class UserServiceImpl extends ServiceImpl implements Us return result; } + @DS("server") @Transactional(rollbackFor = {Exception.class}) @Override public boolean updateUser(User user) { @@ -161,10 +163,11 @@ public class UserServiceImpl extends ServiceImpl implements Us throw new BusinessException("邮箱已存在"); } boolean result = baseMapper.updateById(user) > 0; - if (result && user.getRoles() != null && user.getRoles().size() > 0) { + // roles 非 null 即代表本次提交了角色数据:先清空旧关联,空列表表示清空全部角色 + if (result && user.getRoles() != null) { userRoleService.remove(new LambdaUpdateWrapper().eq(UserRole::getUserId, user.getUserId())); List roleIds = user.getRoles().stream().map(Role::getRoleId).collect(Collectors.toList()); - if (userRoleService.saveBatch(user.getUserId(), roleIds) < roleIds.size()) { + if (!roleIds.isEmpty() && userRoleService.saveBatch(user.getUserId(), roleIds) < roleIds.size()) { throw new BusinessException("用户角色添加失败"); } } diff --git a/src/main/java/com/gxwebsoft/common/system/vo/RoleMemberVO.java b/src/main/java/com/gxwebsoft/common/system/vo/RoleMemberVO.java new file mode 100644 index 0000000..68edf9f --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/vo/RoleMemberVO.java @@ -0,0 +1,40 @@ +package com.gxwebsoft.common.system.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * 角色成员 + * + * @author WebSoft + * @since 2026-10-30 + */ +@Data +@ApiModel(description = "角色成员") +public class RoleMemberVO implements Serializable { + private static final long serialVersionUID = 1L; + + @ApiModelProperty("用户id") + private Integer userId; + + @ApiModelProperty("账号") + private String username; + + @ApiModelProperty("真实姓名") + private String realName; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("所属组织id") + private Integer organizationId; + + @ApiModelProperty("所属组织名称") + private String organizationName; + + @ApiModelProperty("状态, 0正常, 1停用") + private Integer status; +} diff --git a/src/test/java/com/gxwebsoft/approval/RoleMemberIT.java b/src/test/java/com/gxwebsoft/approval/RoleMemberIT.java new file mode 100644 index 0000000..c62a63b --- /dev/null +++ b/src/test/java/com/gxwebsoft/approval/RoleMemberIT.java @@ -0,0 +1,225 @@ +package com.gxwebsoft.approval; + +import com.gxwebsoft.support.CoreTestData; +import com.gxwebsoft.support.TestAuthentication; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +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.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +import javax.sql.DataSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * 角色成员维护与用户清空角色的端到端行为。 + */ +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles({"gxtyzx-test", "test"}) +class RoleMemberIT { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private DataSource dataSource; + + private CoreTestData data; + private String suffix; + + @BeforeEach + void setUp() { + data = new CoreTestData(dataSource); + suffix = String.valueOf(System.nanoTime()); + } + + @AfterEach + void tearDown() { + data.cleanup(); + } + + @Test + void addsMemberAndListsRoleMembers() throws Exception { + int roleId = data.createRole("dept_leader_" + suffix, "部门负责人" + suffix); + int organizationId = data.createOrganization("测试部门-" + suffix); + int userId = data.createUser("member_" + suffix, "张三", organizationId, 0); + + addMember(roleId, userId).andExpect(jsonPath("$.code").value(0)); + assertThat(awaitOperationRecord("/api/system/role-member", String.valueOf(roleId))).isTrue(); + + mockMvc.perform(get("/api/system/role-member/page") + .with(auth("sys:role:update")) + .header("tenantId", TestAuthentication.TENANT_ID) + .param("roleId", String.valueOf(roleId)) + .param("page", "1") + .param("limit", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.count").value(1)) + .andExpect(jsonPath("$.data.list[0].userId").value(userId)) + .andExpect(jsonPath("$.data.list[0].username").value("member_" + suffix)) + .andExpect(jsonPath("$.data.list[0].realName").value("张三")) + .andExpect(jsonPath("$.data.list[0].organizationId").value(organizationId)) + .andExpect(jsonPath("$.data.list[0].organizationName").value("测试部门-" + suffix)) + .andExpect(jsonPath("$.data.list[0].status").value(0)); + } + + @Test + void removingLastMemberLeavesRoleEmpty() throws Exception { + int roleId = data.createRole("approval_archivist_" + suffix, "收文员" + suffix); + int userId = data.createUser("archivist_" + suffix, "李四", null, 0); + addMember(roleId, userId).andExpect(jsonPath("$.code").value(0)); + assertThat(data.roleIdsOf(userId)).containsExactly(roleId); + + mockMvc.perform(delete("/api/system/role-member") + .with(auth("sys:role:update")) + .header("tenantId", TestAuthentication.TENANT_ID) + .param("roleId", String.valueOf(roleId)) + .param("userId", String.valueOf(userId))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)); + + assertThat(data.roleIdsOf(userId)).isEmpty(); + mockMvc.perform(get("/api/system/role-member/page") + .with(auth("sys:role:update")) + .header("tenantId", TestAuthentication.TENANT_ID) + .param("roleId", String.valueOf(roleId)) + .param("page", "1") + .param("limit", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.count").value(0)) + .andExpect(jsonPath("$.data.list.length()").value(0)); + } + + @Test + void clearsAllRolesWhenUserUpdateHasEmptyRoleList() throws Exception { + int roleId = data.createRole("dept_deputy_" + suffix, "部门副职" + suffix); + int userId = data.createUser("deputy_" + suffix, "王五", null, 0); + data.bind(userId, roleId); + assertThat(data.roleIdsOf(userId)).containsExactly(roleId); + + mockMvc.perform(put("/api/system/user") + .with(auth("sys:user:update")) + .header("tenantId", TestAuthentication.TENANT_ID) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"userId\":" + userId + ",\"nickname\":\"改名" + suffix + "\",\"roles\":[]}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)); + + assertThat(data.roleIdsOf(userId)).isEmpty(); + mockMvc.perform(get("/api/system/role-member/page") + .with(auth("sys:role:update")) + .header("tenantId", TestAuthentication.TENANT_ID) + .param("roleId", String.valueOf(roleId)) + .param("page", "1") + .param("limit", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.count").value(0)); + } + + @Test + void pagesRoleMembersAndHidesDisabledUsers() throws Exception { + int roleId = data.createRole("dept_leader_page_" + suffix, "分页部门负责人" + suffix); + int otherRoleId = data.createRole("other_role_" + suffix, "其他角色" + suffix); + int first = data.createUser("page_a_" + suffix, "甲", null, 0); + int second = data.createUser("page_b_" + suffix, "乙", null, 0); + int third = data.createUser("page_c_" + suffix, "丙", null, 0); + int disabled = data.createUser("page_disabled_" + suffix, "停用", null, 1); + int outsider = data.createUser("page_other_" + suffix, "外角色", null, 0); + for (int userId : new int[]{first, second, third, disabled}) { + data.bind(userId, roleId); + } + data.bind(outsider, otherRoleId); + + mockMvc.perform(get("/api/system/role-member/page") + .with(auth("sys:role:update")) + .header("tenantId", TestAuthentication.TENANT_ID) + .param("roleId", String.valueOf(roleId)) + .param("page", "1") + .param("limit", "2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.count").value(3)) + .andExpect(jsonPath("$.data.list.length()").value(2)); + + mockMvc.perform(get("/api/system/role-member/page") + .with(auth("sys:role:update")) + .header("tenantId", TestAuthentication.TENANT_ID) + .param("roleId", String.valueOf(roleId)) + .param("page", "2") + .param("limit", "2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.count").value(3)) + .andExpect(jsonPath("$.data.list.length()").value(1)); + + mockMvc.perform(get("/api/system/role-member/page") + .with(auth("sys:role:update")) + .header("tenantId", TestAuthentication.TENANT_ID) + .param("roleId", String.valueOf(roleId)) + .param("keywords", "page_a_")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.count").value(1)) + .andExpect(jsonPath("$.data.list[0].userId").value(first)); + } + + @Test + void rejectsDuplicateMemberWithChineseMessage() throws Exception { + int roleId = data.createRole("dept_leader_dup_" + suffix, "重复部门负责人" + suffix); + int userId = data.createUser("dup_" + suffix, "赵六", null, 0); + addMember(roleId, userId).andExpect(jsonPath("$.code").value(0)); + + addMember(roleId, userId) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(1)) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("已是角色成员"))); + } + + @Test + void rejectsWithoutRoleUpdateAuthority() throws Exception { + mockMvc.perform(get("/api/system/role-member/page") + .with(auth("sys:role:list")) + .header("tenantId", TestAuthentication.TENANT_ID) + .param("roleId", "586")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(403)); + } + + private org.springframework.test.web.servlet.ResultActions addMember(int roleId, int userId) throws Exception { + return mockMvc.perform(post("/api/system/role-member") + .with(auth("sys:role:update")) + .header("tenantId", TestAuthentication.TENANT_ID) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"roleId\":" + roleId + ",\"userId\":" + userId + "}")) + .andExpect(status().isOk()); + } + + private RequestPostProcessor auth(String... authorities) { + return authentication(TestAuthentication.admin(authorities)); + } + + /* 操作日志异步写入,最多等待 2 秒 */ + private boolean awaitOperationRecord(String url, String paramsMark) throws InterruptedException { + long deadline = System.currentTimeMillis() + 2000; + while (System.currentTimeMillis() < deadline) { + if (data.countOperationRecords(url, paramsMark) > 0) { + return true; + } + Thread.sleep(100); + } + return data.countOperationRecords(url, paramsMark) > 0; + } +} diff --git a/src/test/java/com/gxwebsoft/support/CoreTestData.java b/src/test/java/com/gxwebsoft/support/CoreTestData.java new file mode 100644 index 0000000..4f19c0f --- /dev/null +++ b/src/test/java/com/gxwebsoft/support/CoreTestData.java @@ -0,0 +1,167 @@ +package com.gxwebsoft.support; + +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; + +import javax.sql.DataSource; +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * gxtyzx_core 测试夹具:直接写 sys_user / sys_role / sys_user_role / sys_organization。 + * + *

共享测试库不会被测试事务回滚,调用方必须在用例结束后执行 {@link #cleanup()}。

+ */ +public final class CoreTestData { + + private final JdbcTemplate core; + private final List userIds = new ArrayList<>(); + private final List roleIds = new ArrayList<>(); + private final List organizationIds = new ArrayList<>(); + private final List operationParamMarks = new ArrayList<>(); + + public CoreTestData(DataSource dataSource) { + DataSource server = ((DynamicRoutingDataSource) dataSource).getDataSource("server"); + this.core = new JdbcTemplate(server); + } + + /** + * 创建启用/停用用户,返回用户id。 + */ + public int createUser(String username, String realName, Integer organizationId, int status) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + core.update(connection -> { + PreparedStatement ps = connection.prepareStatement( + "INSERT INTO sys_user (username, password, nickname, real_name, organization_id, status, deleted, tenant_id) " + + "VALUES (?, ?, ?, ?, ?, ?, 0, ?)", Statement.RETURN_GENERATED_KEYS); + ps.setString(1, username); + ps.setString(2, "test-password"); + ps.setString(3, realName); + ps.setString(4, realName); + if (organizationId == null) { + ps.setNull(5, java.sql.Types.INTEGER); + } else { + ps.setInt(5, organizationId); + } + ps.setInt(6, status); + ps.setInt(7, TestAuthentication.TENANT_ID); + return ps; + }, keyHolder); + int userId = keyHolder.getKey().intValue(); + userIds.add(userId); + return userId; + } + + /** + * 创建角色,返回角色id。 + */ + public int createRole(String roleCode, String roleName) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + core.update(connection -> { + PreparedStatement ps = connection.prepareStatement( + "INSERT INTO sys_role (role_name, role_code, deleted, tenant_id) VALUES (?, ?, 0, ?)", + Statement.RETURN_GENERATED_KEYS); + ps.setString(1, roleName); + ps.setString(2, roleCode); + ps.setInt(3, TestAuthentication.TENANT_ID); + return ps; + }, keyHolder); + int roleId = keyHolder.getKey().intValue(); + roleIds.add(roleId); + // 角色成员写操作的操作日志参数里含 roleId,结束时可据此清理 + operationParamMarks.add(String.valueOf(roleId)); + return roleId; + } + + /** + * 创建组织,返回组织id。 + */ + public int createOrganization(String organizationName) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + core.update(connection -> { + PreparedStatement ps = connection.prepareStatement( + "INSERT INTO sys_organization (parent_id, organization_name, organization_type, deleted, tenant_id) " + + "VALUES (0, ?, '', 0, ?)", Statement.RETURN_GENERATED_KEYS); + ps.setString(1, organizationName); + ps.setInt(2, TestAuthentication.TENANT_ID); + return ps; + }, keyHolder); + int organizationId = keyHolder.getKey().intValue(); + organizationIds.add(organizationId); + return organizationId; + } + + /** + * 绑定用户与角色。 + */ + public void bind(int userId, int roleId) { + core.update("INSERT INTO sys_user_role (user_id, role_id, tenant_id) VALUES (?, ?, ?)", + userId, roleId, TestAuthentication.TENANT_ID); + } + + /** + * 查询用户当前的角色id。 + */ + public List roleIdsOf(int userId) { + return core.queryForList("SELECT role_id FROM sys_user_role WHERE user_id = ?", Integer.class, userId); + } + + /** + * 统计包含指定参数标记的操作日志条数。 + */ + public int countOperationRecords(String url, String paramsMark) { + Integer count = core.queryForObject( + "SELECT COUNT(*) FROM sys_operation_record WHERE url = ? AND params LIKE ?", + Integer.class, url, "%" + paramsMark + "%"); + return count == null ? 0 : count; + } + + /** + * 清理本夹具创建的全部行。 + */ + public void cleanup() { + if (!userIds.isEmpty() || !roleIds.isEmpty()) { + List args = new ArrayList<>(); + String userIn = placeholders(userIds.size()); + String roleIn = placeholders(roleIds.size()); + StringBuilder sql = new StringBuilder("DELETE FROM sys_user_role WHERE 1 = 0"); + if (!userIds.isEmpty()) { + sql.append(" OR user_id IN (").append(userIn).append(')'); + args.addAll(userIds); + } + if (!roleIds.isEmpty()) { + sql.append(" OR role_id IN (").append(roleIn).append(')'); + args.addAll(roleIds); + } + core.update(sql.toString(), args.toArray()); + } + if (!userIds.isEmpty()) { + core.update("DELETE FROM sys_user WHERE user_id IN (" + placeholders(userIds.size()) + ")", userIds.toArray()); + } + if (!roleIds.isEmpty()) { + core.update("DELETE FROM sys_role WHERE role_id IN (" + placeholders(roleIds.size()) + ")", roleIds.toArray()); + } + if (!organizationIds.isEmpty()) { + core.update("DELETE FROM sys_organization WHERE organization_id IN (" + placeholders(organizationIds.size()) + ")", + organizationIds.toArray()); + } + if (!operationParamMarks.isEmpty()) { + for (String mark : operationParamMarks) { + core.update("DELETE FROM sys_operation_record WHERE params LIKE ?", "%" + mark + "%"); + } + operationParamMarks.clear(); + } + userIds.clear(); + roleIds.clear(); + organizationIds.clear(); + } + + private static String placeholders(int size) { + return String.join(",", Collections.nCopies(size, "?")); + } +}