feat: 角色成员维护接口与用户清空角色修复
- 新增 /api/system/role-member 分页/添加/移除,@DS("server"),权限 sys:role:update,写操作记操作日志
- UserServiceImpl.updateUser 在 roles 非空判断下先删后插,空列表清空全部角色;updateUser/pageRel 切 server 库
- OperationRecordServiceImpl 加类级 @DS("server"),操作日志真正写入 gxtyzx_core
- OperationLogAspect 兼容无 User-Agent 的客户端,避免写日志 NPE 导致写操作失败
- RoleMemberIT 覆盖添加成员、移除最后一名成员、按角色分页、用户清空角色、403 与操作日志
This commit is contained in:
@@ -40,6 +40,13 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- spring-security-test: MockMvc 直接注入登录态 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- spring-boot-web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
// 记录异常信息
|
||||
|
||||
@@ -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<PageResult<RoleMemberVO>> 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("移除成功");
|
||||
}
|
||||
}
|
||||
@@ -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<RoleMemberVO>
|
||||
*/
|
||||
List<RoleMemberVO> selectPageByRoleId(@Param("page") IPage<RoleMemberVO> page,
|
||||
@Param("roleId") Integer roleId,
|
||||
@Param("keywords") String keywords);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||
<mapper namespace="com.gxwebsoft.common.system.mapper.RoleMemberMapper">
|
||||
|
||||
<!-- 角色下的启用用户 -->
|
||||
<select id="selectPageByRoleId" resultType="com.gxwebsoft.common.system.vo.RoleMemberVO">
|
||||
SELECT a.user_id,
|
||||
a.username,
|
||||
a.real_name,
|
||||
a.nickname,
|
||||
a.organization_id,
|
||||
a.status,
|
||||
c.organization_name
|
||||
FROM sys_user_role b
|
||||
INNER JOIN sys_user a ON a.user_id = b.user_id AND a.deleted = 0 AND a.status = 0
|
||||
LEFT JOIN sys_organization c ON c.organization_id = a.organization_id AND c.deleted = 0
|
||||
WHERE b.role_id = #{roleId}
|
||||
<if test="keywords != null and keywords != ''">
|
||||
AND (a.username LIKE CONCAT('%', #{keywords}, '%')
|
||||
OR a.real_name LIKE CONCAT('%', #{keywords}, '%')
|
||||
OR a.nickname LIKE CONCAT('%', #{keywords}, '%'))
|
||||
</if>
|
||||
ORDER BY a.user_id DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<RoleMemberVO>
|
||||
*/
|
||||
PageResult<RoleMemberVO> 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);
|
||||
}
|
||||
@@ -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<OperationRecordMapper, OperationRecord>
|
||||
implements OperationRecordService {
|
||||
|
||||
|
||||
@@ -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<RoleMemberVO> pageMembers(RoleMemberParam param) {
|
||||
PageParam<RoleMemberVO, RoleMemberParam> page = new PageParam<>(param);
|
||||
List<RoleMemberVO> 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<UserRole>()
|
||||
.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<UserRole>()
|
||||
.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("角色不存在");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@DS("server")
|
||||
@Override
|
||||
public PageResult<User> pageRel(UserParam param) {
|
||||
PageParam<User, UserParam> page = new PageParam<>(param);
|
||||
@@ -142,6 +143,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> 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<UserMapper, User> 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<UserRole>().eq(UserRole::getUserId, user.getUserId()));
|
||||
List<Integer> 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("用户角色添加失败");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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。
|
||||
*
|
||||
* <p>共享测试库不会被测试事务回滚,调用方必须在用例结束后执行 {@link #cleanup()}。</p>
|
||||
*/
|
||||
public final class CoreTestData {
|
||||
|
||||
private final JdbcTemplate core;
|
||||
private final List<Integer> userIds = new ArrayList<>();
|
||||
private final List<Integer> roleIds = new ArrayList<>();
|
||||
private final List<Integer> organizationIds = new ArrayList<>();
|
||||
private final List<String> 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<Integer> 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<Object> 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, "?"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user