feat(wm-system): #74 系统管理(角色/用户/部门/日志/字典)

- 增强 SysRole: 5级角色(level 1-5), 数据权限(dataScope), 权限配置(permissions JSON)
- 增强 SysUser: 角色关联(roleId), 最后登录记录, 分页查询+多维度筛选
- 增强 SysDepartment: 树结构(ancestors), 子部门查询, 树形构建
- 增强 SysLog: 状态/错误信息字段, 分页+多条件筛选(类型/用户/模块/时间/状态)
- 新增 DataDictionary + DataDictionaryItem: 字典CRUD+字典项CRUD, 按编码查询
- 5个专用Controller: RoleController/UserController/DepartmentController/LogController/DictionaryController
- 28个RESTful API端点于 /api/system/*
- 5个专用Service接口+实现: RoleService/UserService/DepartmentService/LogService/DictionaryService
- DDL: V2__system_enhance.sql — 新增表+索引+初始数据
- 单元测试: 5个Service测试类, 覆盖核心业务逻辑
- DTO: 请求参数校验(jakarta.validation)
- 保持SysService向后兼容
This commit is contained in:
2026-06-14 15:23:34 +08:00
parent 520b4084eb
commit 51c292f25b
43 changed files with 2199 additions and 68 deletions
@@ -0,0 +1,93 @@
package com.water.system.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.common.core.exception.BusinessException;
import com.water.system.entity.SysDepartment;
import com.water.system.entity.dto.DeptRequest;
import com.water.system.mapper.SysDepartmentMapper;
import com.water.system.service.impl.DepartmentServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class DepartmentServiceImplTest {
@Mock
private SysDepartmentMapper deptMapper;
@InjectMocks
private DepartmentServiceImpl departmentService;
@Test
void getDeptTree_shouldBuildTree() {
SysDepartment root = new SysDepartment();
root.setId(1L);
root.setParentId(0L);
root.setDeptName("总公司");
SysDepartment child1 = new SysDepartment();
child1.setId(2L);
child1.setParentId(1L);
child1.setDeptName("技术部");
SysDepartment child2 = new SysDepartment();
child2.setId(3L);
child2.setParentId(1L);
child2.setDeptName("市场部");
when(deptMapper.selectList(any(LambdaQueryWrapper.class)))
.thenReturn(Arrays.asList(root, child1, child2));
List<SysDepartment> tree = departmentService.getDeptTree();
assertEquals(1, tree.size());
assertEquals("总公司", tree.get(0).getDeptName());
assertEquals(2, tree.get(0).getChildren().size());
}
@Test
void createDept_shouldSetAncestors() {
SysDepartment parent = new SysDepartment();
parent.setId(1L);
parent.setAncestors("0");
parent.setDeptName("总公司");
when(deptMapper.selectById(1L)).thenReturn(parent);
when(deptMapper.insert(any(SysDepartment.class))).thenAnswer(inv -> {
SysDepartment d = inv.getArgument(0);
d.setId(10L);
assertEquals("0,1", d.getAncestors());
return 1;
});
DeptRequest request = new DeptRequest();
request.setDeptName("研发部");
request.setParentId(1L);
Long id = departmentService.createDept(request);
assertEquals(10L, id);
}
@Test
void deleteDept_shouldFailWithChildren() {
when(deptMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(2L);
assertThrows(BusinessException.class, () -> departmentService.deleteDept(1L));
}
@Test
void deleteDept_shouldSucceedWithoutChildren() {
when(deptMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
assertDoesNotThrow(() -> departmentService.deleteDept(5L));
verify(deptMapper).deleteById(5L);
}
}
@@ -0,0 +1,104 @@
package com.water.system.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.common.core.exception.BusinessException;
import com.water.system.entity.DataDictionary;
import com.water.system.entity.DataDictionaryItem;
import com.water.system.entity.dto.DictItemRequest;
import com.water.system.entity.dto.DictRequest;
import com.water.system.mapper.DataDictionaryMapper;
import com.water.system.mapper.DataDictionaryItemMapper;
import com.water.system.service.impl.DictionaryServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class DictionaryServiceImplTest {
@Mock
private DataDictionaryMapper dictMapper;
@Mock
private DataDictionaryItemMapper itemMapper;
@InjectMocks
private DictionaryServiceImpl dictionaryService;
@Test
void createDictionary_shouldSucceed() {
DictRequest request = new DictRequest();
request.setDictCode("water_quality_level");
request.setDictName("水质等级");
when(dictMapper.selectByDictCode("water_quality_level")).thenReturn(null);
when(dictMapper.insert(any(DataDictionary.class))).thenAnswer(inv -> {
DataDictionary d = inv.getArgument(0);
d.setId(10L);
return 1;
});
Long id = dictionaryService.createDictionary(request);
assertEquals(10L, id);
}
@Test
void createDictionary_shouldFailOnDuplicate() {
DictRequest request = new DictRequest();
request.setDictCode("sys_user_status");
DataDictionary existing = new DataDictionary();
existing.setDictCode("sys_user_status");
when(dictMapper.selectByDictCode("sys_user_status")).thenReturn(existing);
assertThrows(BusinessException.class, () -> dictionaryService.createDictionary(request));
}
@Test
void listItemsByDictCode_shouldReturnItems() {
DataDictionaryItem item1 = new DataDictionaryItem();
item1.setItemCode("ENABLED");
item1.setItemName("正常");
DataDictionaryItem item2 = new DataDictionaryItem();
item2.setItemCode("DISABLED");
item2.setItemName("停用");
when(itemMapper.selectByDictCode("sys_user_status")).thenReturn(Arrays.asList(item1, item2));
List<DataDictionaryItem> items = dictionaryService.listItemsByDictCode("sys_user_status");
assertEquals(2, items.size());
}
@Test
void deleteDictionary_shouldDeleteItemsToo() {
dictionaryService.deleteDictionary(1L);
verify(itemMapper).delete(any(LambdaQueryWrapper.class));
verify(dictMapper).deleteById(1L);
}
@Test
void createItem_shouldSucceed() {
DictItemRequest request = new DictItemRequest();
request.setDictId(1L);
request.setItemCode("NEW_ITEM");
request.setItemName("新项");
request.setItemValue("value");
when(itemMapper.insert(any(DataDictionaryItem.class))).thenAnswer(inv -> {
DataDictionaryItem i = inv.getArgument(0);
i.setId(20L);
return 1;
});
Long id = dictionaryService.createItem(request);
assertEquals(20L, id);
}
}
@@ -0,0 +1,93 @@
package com.water.system.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.system.entity.SysLog;
import com.water.system.entity.dto.LogQueryRequest;
import com.water.system.mapper.SysLogMapper;
import com.water.system.service.impl.LogServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class LogServiceImplTest {
@Mock
private SysLogMapper logMapper;
@InjectMocks
private LogServiceImpl logService;
@Test
void pageLogs_shouldReturnPagedResult() {
Page<SysLog> page = new Page<>(1, 10);
SysLog log1 = new SysLog();
log1.setId(1L);
log1.setLogType("LOGIN");
log1.setUsername("admin");
log1.setCreatedTime(LocalDateTime.now());
SysLog log2 = new SysLog();
log2.setId(2L);
log2.setLogType("OPERATE");
log2.setUsername("user1");
log2.setModule("用户管理");
page.setRecords(Arrays.asList(log1, log2));
page.setTotal(2);
when(logMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(page);
LogQueryRequest request = new LogQueryRequest();
request.setPageNum(1);
request.setPageSize(10);
IPage<SysLog> result = logService.pageLogs(request);
assertEquals(2, result.getTotal());
assertEquals("LOGIN", result.getRecords().get(0).getLogType());
}
@Test
void pageLogs_withFilters_shouldApplyAllConditions() {
Page<SysLog> page = new Page<>(1, 10);
page.setRecords(Arrays.asList());
page.setTotal(0);
when(logMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(page);
LogQueryRequest request = new LogQueryRequest();
request.setLogType("OPERATE");
request.setUsername("admin");
request.setModule("系统管理");
request.setStatus(1);
request.setStartTime("2024-01-01");
request.setEndTime("2024-12-31");
IPage<SysLog> result = logService.pageLogs(request);
assertNotNull(result);
verify(logMapper).selectPage(any(Page.class), any(LambdaQueryWrapper.class));
}
@Test
void saveLog_shouldInsert() {
SysLog log = new SysLog();
log.setLogType("LOGIN");
log.setUsername("testuser");
log.setIp("127.0.0.1");
logService.saveLog(log);
verify(logMapper).insert(log);
}
}
@@ -0,0 +1,115 @@
package com.water.system.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.common.core.exception.BusinessException;
import com.water.system.entity.SysRole;
import com.water.system.entity.dto.RoleRequest;
import com.water.system.mapper.SysRoleMapper;
import com.water.system.service.impl.RoleServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class RoleServiceImplTest {
@Mock
private SysRoleMapper roleMapper;
@InjectMocks
private RoleServiceImpl roleService;
private final ObjectMapper objectMapper = new ObjectMapper();
@BeforeEach
void setUp() {
// ObjectMapper is final, inject manually via reflection or just let it be used
}
@Test
void listRoles_shouldReturnAllRoles() {
SysRole admin = new SysRole();
admin.setId(1L);
admin.setRoleCode("SUPER_ADMIN");
admin.setLevel(1);
SysRole user = new SysRole();
user.setId(4L);
user.setRoleCode("EMPLOYEE");
user.setLevel(4);
when(roleMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Arrays.asList(admin, user));
List<SysRole> roles = roleService.listRoles();
assertEquals(2, roles.size());
assertEquals("SUPER_ADMIN", roles.get(0).getRoleCode());
}
@Test
void createRole_shouldSucceed() {
RoleRequest request = new RoleRequest();
request.setRoleCode("TEST_ROLE");
request.setRoleName("测试角色");
request.setLevel(3);
request.setPermissions(Arrays.asList("user:read", "user:write"));
when(roleMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
when(roleMapper.insert(any(SysRole.class))).thenAnswer(inv -> {
SysRole r = inv.getArgument(0);
r.setId(10L);
return 1;
});
Long id = roleService.createRole(request);
assertEquals(10L, id);
verify(roleMapper).insert(any(SysRole.class));
}
@Test
void createRole_shouldFailOnDuplicateCode() {
RoleRequest request = new RoleRequest();
request.setRoleCode("SUPER_ADMIN");
request.setRoleName("重复角色");
request.setLevel(1);
when(roleMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(1L);
assertThrows(BusinessException.class, () -> roleService.createRole(request));
}
@Test
void deleteRole_shouldFailForAdminRoles() {
SysRole admin = new SysRole();
admin.setId(1L);
admin.setLevel(1);
admin.setRoleCode("SUPER_ADMIN");
when(roleMapper.selectById(1L)).thenReturn(admin);
assertThrows(BusinessException.class, () -> roleService.deleteRole(1L));
}
@Test
void deleteRole_shouldSucceedForNormalRoles() {
SysRole role = new SysRole();
role.setId(5L);
role.setLevel(4);
role.setRoleCode("EMPLOYEE");
when(roleMapper.selectById(5L)).thenReturn(role);
assertDoesNotThrow(() -> roleService.deleteRole(5L));
verify(roleMapper).deleteById(5L);
}
}
@@ -0,0 +1,120 @@
package com.water.system.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.common.core.exception.BusinessException;
import com.water.system.entity.SysUser;
import com.water.system.entity.dto.UserCreateRequest;
import com.water.system.entity.dto.UserUpdateRequest;
import com.water.system.mapper.SysUserMapper;
import com.water.system.service.impl.UserServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {
@Mock
private SysUserMapper userMapper;
@InjectMocks
private UserServiceImpl userService;
@Test
void createUser_shouldSucceed() {
UserCreateRequest request = new UserCreateRequest();
request.setUsername("testuser");
request.setPassword("test123");
request.setRealName("测试用户");
request.setPhone("13800138000");
when(userMapper.selectByUsername("testuser")).thenReturn(null);
when(userMapper.insert(any(SysUser.class))).thenAnswer(inv -> {
SysUser u = inv.getArgument(0);
u.setId(100L);
return 1;
});
Long id = userService.createUser(request);
assertEquals(100L, id);
verify(userMapper).insert(any(SysUser.class));
}
@Test
void createUser_shouldFailOnDuplicateUsername() {
UserCreateRequest request = new UserCreateRequest();
request.setUsername("admin");
request.setPassword("pass");
SysUser existing = new SysUser();
existing.setUsername("admin");
when(userMapper.selectByUsername("admin")).thenReturn(existing);
assertThrows(BusinessException.class, () -> userService.createUser(request));
}
@Test
void updateStatus_shouldSucceed() {
SysUser user = new SysUser();
user.setId(1L);
user.setStatus(1);
when(userMapper.selectById(1L)).thenReturn(user);
userService.updateStatus(1L, 0);
verify(userMapper).updateById(argThat(u -> u.getStatus() == 0));
}
@Test
void deleteUser_shouldFailForAdmin() {
SysUser admin = new SysUser();
admin.setId(1L);
admin.setUsername("admin");
when(userMapper.selectById(1L)).thenReturn(admin);
assertThrows(BusinessException.class, () -> userService.deleteUser(1L));
}
@Test
void resetPassword_shouldSucceed() {
SysUser user = new SysUser();
user.setId(5L);
user.setUsername("testuser");
when(userMapper.selectById(5L)).thenReturn(user);
assertDoesNotThrow(() -> userService.resetPassword(5L, "newPass123"));
verify(userMapper).updateById(argThat(u -> u.getPassword() != null));
}
@Test
void pageUsers_shouldReturnPagedResult() {
Page<SysUser> page = new Page<>(1, 10);
SysUser user1 = new SysUser();
user1.setId(1L);
user1.setUsername("user1");
user1.setPassword("hashed");
SysUser user2 = new SysUser();
user2.setId(2L);
user2.setUsername("user2");
user2.setPassword("hashed");
page.setRecords(Arrays.asList(user1, user2));
page.setTotal(2);
when(userMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(page);
IPage<SysUser> result = userService.pageUsers(1, 10, null, null, null);
assertEquals(2, result.getTotal());
// 密码应被清除
assertNull(result.getRecords().get(0).getPassword());
}
}