Commit b608dd84b4c5c3a1ffed4488e7168a22cb6542f2

Authored by zichun
1 parent fe2e6306

feat(usr): user query service REQ-USR-003

backend/src/main/java/com/xly/erp/module/usr/service/UserService.java
1 package com.xly.erp.module.usr.service; 1 package com.xly.erp.module.usr.service;
2 2
  3 +import com.xly.erp.common.response.PageResult;
3 import com.xly.erp.module.usr.dto.UserCreateDTO; 4 import com.xly.erp.module.usr.dto.UserCreateDTO;
  5 +import com.xly.erp.module.usr.dto.UserQueryDTO;
4 import com.xly.erp.module.usr.dto.UserUpdateDTO; 6 import com.xly.erp.module.usr.dto.UserUpdateDTO;
  7 +import com.xly.erp.module.usr.vo.UserListItemVO;
5 import com.xly.erp.module.usr.vo.UserVO; 8 import com.xly.erp.module.usr.vo.UserVO;
6 9
7 public interface UserService { 10 public interface UserService {
@@ -10,4 +13,7 @@ public interface UserService { @@ -10,4 +13,7 @@ public interface UserService {
10 13
11 /** REQ-USR-002 用户修改 */ 14 /** REQ-USR-002 用户修改 */
12 UserVO update(Integer id, UserUpdateDTO dto); 15 UserVO update(Integer id, UserUpdateDTO dto);
  16 +
  17 + /** REQ-USR-003 用户列表查询 */
  18 + PageResult<UserListItemVO> search(UserQueryDTO query);
13 } 19 }
backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java
1 package com.xly.erp.module.usr.service.impl; 1 package com.xly.erp.module.usr.service.impl;
2 2
3 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 3 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.metadata.IPage;
  5 +import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4 import com.xly.erp.common.exception.BizException; 6 import com.xly.erp.common.exception.BizException;
5 import com.xly.erp.common.response.ErrorCode; 7 import com.xly.erp.common.response.ErrorCode;
  8 +import com.xly.erp.common.response.PageResult;
6 import com.xly.erp.module.usr.dto.UserCreateDTO; 9 import com.xly.erp.module.usr.dto.UserCreateDTO;
  10 +import com.xly.erp.module.usr.dto.UserQueryDTO;
7 import com.xly.erp.module.usr.dto.UserUpdateDTO; 11 import com.xly.erp.module.usr.dto.UserUpdateDTO;
8 import com.xly.erp.module.usr.entity.PermissionCategoryEntity; 12 import com.xly.erp.module.usr.entity.PermissionCategoryEntity;
9 import com.xly.erp.module.usr.entity.StaffEntity; 13 import com.xly.erp.module.usr.entity.StaffEntity;
@@ -14,6 +18,7 @@ import com.xly.erp.module.usr.mapper.StaffMapper; @@ -14,6 +18,7 @@ import com.xly.erp.module.usr.mapper.StaffMapper;
14 import com.xly.erp.module.usr.mapper.UserMapper; 18 import com.xly.erp.module.usr.mapper.UserMapper;
15 import com.xly.erp.module.usr.mapper.UserPermissionMapper; 19 import com.xly.erp.module.usr.mapper.UserPermissionMapper;
16 import com.xly.erp.module.usr.service.UserService; 20 import com.xly.erp.module.usr.service.UserService;
  21 +import com.xly.erp.module.usr.vo.UserListItemVO;
17 import com.xly.erp.module.usr.vo.UserVO; 22 import com.xly.erp.module.usr.vo.UserVO;
18 import lombok.RequiredArgsConstructor; 23 import lombok.RequiredArgsConstructor;
19 import org.springframework.dao.DuplicateKeyException; 24 import org.springframework.dao.DuplicateKeyException;
@@ -24,6 +29,8 @@ import org.springframework.transaction.annotation.Transactional; @@ -24,6 +29,8 @@ import org.springframework.transaction.annotation.Transactional;
24 import java.time.LocalDateTime; 29 import java.time.LocalDateTime;
25 import java.util.ArrayList; 30 import java.util.ArrayList;
26 import java.util.List; 31 import java.util.List;
  32 +import java.util.Map;
  33 +import java.util.Set;
27 34
28 /** REQ-USR-001 用户新增 */ 35 /** REQ-USR-001 用户新增 */
29 @Service 36 @Service
@@ -162,4 +169,47 @@ public class UserServiceImpl implements UserService { @@ -162,4 +169,47 @@ public class UserServiceImpl implements UserService {
162 169
163 return UserVO.from(target, categoryIds); 170 return UserVO.from(target, categoryIds);
164 } 171 }
  172 +
  173 + /** REQ-USR-003 用户列表查询 — queryField 白名单映射 + LEFT JOIN tStaff 分页 */
  174 + private static final Map<String, String> QUERY_COLUMN_MAP = Map.ofEntries(
  175 + Map.entry("username", "u.sUserName"),
  176 + Map.entry("staffname", "s.sStaffName"),
  177 + Map.entry("userno", "u.sUserNo"),
  178 + Map.entry("department", "s.sDepartment"),
  179 + Map.entry("usertype", "u.sUserType"),
  180 + Map.entry("language", "u.sLanguage"),
  181 + Map.entry("deleted", "u.bDeleted"),
  182 + Map.entry("lastLoginDate", "u.tLastLoginDate"),
  183 + Map.entry("createdBy", "u.sCreatedBy"));
  184 +
  185 + private static final Set<String> MATCH_TYPES = Set.of("contains", "notContains", "equals");
  186 +
  187 + @Override
  188 + @Transactional(readOnly = true)
  189 + public PageResult<UserListItemVO> search(UserQueryDTO query) {
  190 + // 1. queryField 白名单 + 列映射(防 SQL 注入)
  191 + if (query.getQueryField() != null && !query.getQueryField().isEmpty()) {
  192 + String mapped = QUERY_COLUMN_MAP.get(query.getQueryField());
  193 + if (mapped == null) {
  194 + throw new BizException(ErrorCode.PARAM_INVALID, "queryField 非法: " + query.getQueryField());
  195 + }
  196 + query.setColumn(mapped);
  197 + }
  198 +
  199 + // 2. matchType 白名单
  200 + if (query.getMatchType() != null && !query.getMatchType().isEmpty()
  201 + && !MATCH_TYPES.contains(query.getMatchType())) {
  202 + throw new BizException(ErrorCode.PARAM_INVALID, "matchType 非法: " + query.getMatchType());
  203 + }
  204 +
  205 + // 3. 默认值兜底
  206 + int pageNum = query.getPageNum() == null ? 1 : query.getPageNum();
  207 + int pageSize = query.getPageSize() == null ? 20 : query.getPageSize();
  208 +
  209 + // 4. MP 分页查询
  210 + IPage<UserListItemVO> page = new Page<>(pageNum, pageSize);
  211 + IPage<UserListItemVO> result = userMapper.searchUsers(page, query);
  212 +
  213 + return PageResult.of(result);
  214 + }
165 } 215 }
backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java
1 package com.xly.erp.module.usr.service; 1 package com.xly.erp.module.usr.service;
2 2
3 import com.baomidou.mybatisplus.core.conditions.Wrapper; 3 import com.baomidou.mybatisplus.core.conditions.Wrapper;
  4 +import com.baomidou.mybatisplus.core.metadata.IPage;
  5 +import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4 import com.xly.erp.common.exception.BizException; 6 import com.xly.erp.common.exception.BizException;
5 import com.xly.erp.common.response.ErrorCode; 7 import com.xly.erp.common.response.ErrorCode;
  8 +import com.xly.erp.common.response.PageResult;
6 import com.xly.erp.module.usr.dto.UserCreateDTO; 9 import com.xly.erp.module.usr.dto.UserCreateDTO;
  10 +import com.xly.erp.module.usr.dto.UserQueryDTO;
7 import com.xly.erp.module.usr.dto.UserUpdateDTO; 11 import com.xly.erp.module.usr.dto.UserUpdateDTO;
8 import com.xly.erp.module.usr.entity.PermissionCategoryEntity; 12 import com.xly.erp.module.usr.entity.PermissionCategoryEntity;
9 import com.xly.erp.module.usr.entity.StaffEntity; 13 import com.xly.erp.module.usr.entity.StaffEntity;
@@ -14,6 +18,7 @@ import com.xly.erp.module.usr.mapper.StaffMapper; @@ -14,6 +18,7 @@ import com.xly.erp.module.usr.mapper.StaffMapper;
14 import com.xly.erp.module.usr.mapper.UserMapper; 18 import com.xly.erp.module.usr.mapper.UserMapper;
15 import com.xly.erp.module.usr.mapper.UserPermissionMapper; 19 import com.xly.erp.module.usr.mapper.UserPermissionMapper;
16 import com.xly.erp.module.usr.service.impl.UserServiceImpl; 20 import com.xly.erp.module.usr.service.impl.UserServiceImpl;
  21 +import com.xly.erp.module.usr.vo.UserListItemVO;
17 import com.xly.erp.module.usr.vo.UserVO; 22 import com.xly.erp.module.usr.vo.UserVO;
18 import org.junit.jupiter.api.Test; 23 import org.junit.jupiter.api.Test;
19 import org.junit.jupiter.api.extension.ExtendWith; 24 import org.junit.jupiter.api.extension.ExtendWith;
@@ -410,4 +415,74 @@ class UserServiceImplTest { @@ -410,4 +415,74 @@ class UserServiceImplTest {
410 verify(userPermissionMapper).delete(any(Wrapper.class)); 415 verify(userPermissionMapper).delete(any(Wrapper.class));
411 verify(userPermissionMapper, never()).insert((UserPermissionEntity) any()); 416 verify(userPermissionMapper, never()).insert((UserPermissionEntity) any());
412 } 417 }
  418 +
  419 + // ============================================================
  420 + // REQ-USR-003 search 系列
  421 + // ============================================================
  422 +
  423 + @Test
  424 + void search_emptyDb_returnsEmptyPage() {
  425 + UserQueryDTO query = new UserQueryDTO();
  426 + IPage<UserListItemVO> emptyPage = new Page<>(1, 20);
  427 + emptyPage.setTotal(0L);
  428 + when(userMapper.searchUsers(any(IPage.class), any(UserQueryDTO.class))).thenReturn(emptyPage);
  429 +
  430 + PageResult<UserListItemVO> result = service.search(query);
  431 + assertThat(result.getTotal()).isZero();
  432 + assertThat(result.getList()).isEmpty();
  433 + }
  434 +
  435 + @Test
  436 + void search_invalidQueryField_throws40010() {
  437 + UserQueryDTO query = new UserQueryDTO();
  438 + query.setQueryField("invalid_field");
  439 +
  440 + assertThatThrownBy(() -> service.search(query))
  441 + .isInstanceOf(BizException.class)
  442 + .extracting(e -> ((BizException) e).getCode())
  443 + .isEqualTo(ErrorCode.PARAM_INVALID.getCode());
  444 + }
  445 +
  446 + @Test
  447 + void search_invalidMatchType_throws40010() {
  448 + UserQueryDTO query = new UserQueryDTO();
  449 + query.setMatchType("like"); // 非白名单
  450 +
  451 + assertThatThrownBy(() -> service.search(query))
  452 + .isInstanceOf(BizException.class)
  453 + .extracting(e -> ((BizException) e).getCode())
  454 + .isEqualTo(ErrorCode.PARAM_INVALID.getCode());
  455 + }
  456 +
  457 + @Test
  458 + void search_passesMappedColumnToMapper() {
  459 + UserQueryDTO query = new UserQueryDTO();
  460 + query.setQueryField("username");
  461 + query.setQueryValue("alice");
  462 + IPage<UserListItemVO> emptyPage = new Page<>(1, 20);
  463 + when(userMapper.searchUsers(any(IPage.class), any(UserQueryDTO.class))).thenReturn(emptyPage);
  464 +
  465 + service.search(query);
  466 +
  467 + ArgumentCaptor<UserQueryDTO> cap = ArgumentCaptor.forClass(UserQueryDTO.class);
  468 + verify(userMapper).searchUsers(any(IPage.class), cap.capture());
  469 + assertThat(cap.getValue().getColumn()).isEqualTo("u.sUserName");
  470 + }
  471 +
  472 + @Test
  473 + void search_appliesDefaultPagination_whenNullPageNumOrSize() {
  474 + UserQueryDTO query = new UserQueryDTO();
  475 + query.setPageNum(null);
  476 + query.setPageSize(null);
  477 + IPage<UserListItemVO> emptyPage = new Page<>(1, 20);
  478 + when(userMapper.searchUsers(any(IPage.class), any(UserQueryDTO.class))).thenReturn(emptyPage);
  479 +
  480 + service.search(query);
  481 +
  482 + ArgumentCaptor<IPage> pageCap = ArgumentCaptor.forClass(IPage.class);
  483 + verify(userMapper).searchUsers(pageCap.capture(), any(UserQueryDTO.class));
  484 + IPage<?> page = pageCap.getValue();
  485 + assertThat(page.getCurrent()).isEqualTo(1L);
  486 + assertThat(page.getSize()).isEqualTo(20L);
  487 + }
413 } 488 }