Commit 57a1bbfc7d7a7fdd4e68df6f8d16006fdff3b6c6

Authored by zichun
1 parent 9895c567

feat(usr): user list service + field/match mapping REQ-USR-003

backend/src/main/java/com/xly/erp/module/usr/service/UserService.java
@@ -9,4 +9,6 @@ public interface UserService { @@ -9,4 +9,6 @@ public interface UserService {
9 Map<String, Object> create(CreateUserDTO dto); 9 Map<String, Object> create(CreateUserDTO dto);
10 10
11 Integer update(Integer id, UpdateUserDTO dto); 11 Integer update(Integer id, UpdateUserDTO dto);
  12 +
  13 + Map<String, Object> list(String field, String match, String value, Integer pageNum, Integer pageSize);
12 } 14 }
backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java
@@ -7,6 +7,7 @@ import com.xly.erp.common.security.SecurityContextHelper; @@ -7,6 +7,7 @@ import com.xly.erp.common.security.SecurityContextHelper;
7 import com.xly.erp.module.usr.dto.CreateUserDTO; 7 import com.xly.erp.module.usr.dto.CreateUserDTO;
8 import com.xly.erp.module.usr.dto.UpdateUserDTO; 8 import com.xly.erp.module.usr.dto.UpdateUserDTO;
9 import com.xly.erp.module.usr.entity.User; 9 import com.xly.erp.module.usr.entity.User;
  10 +import com.xly.erp.module.usr.vo.UserListVO;
10 import com.xly.erp.module.usr.entity.UserPermission; 11 import com.xly.erp.module.usr.entity.UserPermission;
11 import com.xly.erp.module.usr.mapper.PermissionCategoryMapper; 12 import com.xly.erp.module.usr.mapper.PermissionCategoryMapper;
12 import com.xly.erp.module.usr.mapper.StaffMapper; 13 import com.xly.erp.module.usr.mapper.StaffMapper;
@@ -18,7 +19,9 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -18,7 +19,9 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
18 import org.springframework.stereotype.Service; 19 import org.springframework.stereotype.Service;
19 import org.springframework.transaction.annotation.Transactional; 20 import org.springframework.transaction.annotation.Transactional;
20 21
  22 +import java.time.LocalDate;
21 import java.time.LocalDateTime; 23 import java.time.LocalDateTime;
  24 +import java.time.format.DateTimeParseException;
22 import java.util.LinkedHashMap; 25 import java.util.LinkedHashMap;
23 import java.util.List; 26 import java.util.List;
24 import java.util.Map; 27 import java.util.Map;
@@ -32,6 +35,30 @@ public class UserServiceImpl implements UserService { @@ -32,6 +35,30 @@ public class UserServiceImpl implements UserService {
32 static final Set<String> LANGUAGES = Set.of("zh", "en", "zh-TW"); 35 static final Set<String> LANGUAGES = Set.of("zh", "en", "zh-TW");
33 static final String DEFAULT_PASSWORD = "666666"; 36 static final String DEFAULT_PASSWORD = "666666";
34 37
  38 + static final int MAX_PAGE_SIZE = 100;
  39 + static final String STRING_TYPE = "STRING";
  40 + static final String BOOLEAN_TYPE = "BOOLEAN";
  41 + static final String DATE_TYPE = "DATE";
  42 +
  43 + private static final Map<String, FieldDef> FIELD_MAP = Map.of(
  44 + "用户名", new FieldDef("u.sUserName", STRING_TYPE),
  45 + "员工名", new FieldDef("s.sStaffName", STRING_TYPE),
  46 + "用户号", new FieldDef("u.sUserNo", STRING_TYPE),
  47 + "部门", new FieldDef("s.sDepartment", STRING_TYPE),
  48 + "用户类型", new FieldDef("u.sUserType", STRING_TYPE),
  49 + "作废", new FieldDef("u.bDeleted", BOOLEAN_TYPE),
  50 + "登录日期", new FieldDef("DATE(u.tLastLoginDate)", DATE_TYPE),
  51 + "制单人", new FieldDef("u.sCreatedBy", STRING_TYPE)
  52 + );
  53 +
  54 + private static final Map<String, String> MATCH_MAP = Map.of(
  55 + "包含", "contains",
  56 + "不包含", "notContains",
  57 + "等于", "equals"
  58 + );
  59 +
  60 + private record FieldDef(String column, String type) {}
  61 +
35 private final UserMapper userMapper; 62 private final UserMapper userMapper;
36 private final UserPermissionMapper userPermissionMapper; 63 private final UserPermissionMapper userPermissionMapper;
37 private final StaffMapper staffMapper; 64 private final StaffMapper staffMapper;
@@ -171,4 +198,63 @@ public class UserServiceImpl implements UserService { @@ -171,4 +198,63 @@ public class UserServiceImpl implements UserService {
171 } 198 }
172 return id; 199 return id;
173 } 200 }
  201 +
  202 + @Override
  203 + @Transactional(readOnly = true)
  204 + public Map<String, Object> list(String field, String match, String value, Integer pageNum, Integer pageSize) {
  205 + int p = (pageNum == null || pageNum < 1) ? 1 : pageNum;
  206 + int size = (pageSize == null) ? 20 : pageSize;
  207 + if (size > MAX_PAGE_SIZE) {
  208 + throw new BizException(40002, "pageSize 超过 100");
  209 + }
  210 + String f = (field == null || field.isBlank()) ? "用户名" : field.trim();
  211 + String m = (match == null || match.isBlank()) ? "包含" : match.trim();
  212 + String v = value == null ? "" : value.trim();
  213 +
  214 + FieldDef def = FIELD_MAP.get(f);
  215 + if (def == null) {
  216 + throw new BizException(40001, "field 取值非法");
  217 + }
  218 + String matchOp = MATCH_MAP.get(m);
  219 + if (matchOp == null) {
  220 + throw new BizException(40001, "match 取值非法");
  221 + }
  222 + if (!STRING_TYPE.equals(def.type()) && !"equals".equals(matchOp)) {
  223 + throw new BizException(40001, "field/match 组合非法");
  224 + }
  225 +
  226 + Object queryValue = v;
  227 + if (!v.isEmpty()) {
  228 + if (BOOLEAN_TYPE.equals(def.type())) {
  229 + queryValue = parseBoolean(v);
  230 + } else if (DATE_TYPE.equals(def.type())) {
  231 + try {
  232 + queryValue = LocalDate.parse(v).toString();
  233 + } catch (DateTimeParseException e) {
  234 + throw new BizException(40001, "登录日期值格式非法(应为 YYYY-MM-DD)");
  235 + }
  236 + }
  237 + } else {
  238 + queryValue = "";
  239 + }
  240 +
  241 + int offset = (p - 1) * size;
  242 + List<UserListVO> records = userMapper.pageWithFilter(def.column(), matchOp, queryValue, offset, size);
  243 + long total = userMapper.countWithFilter(def.column(), matchOp, queryValue);
  244 +
  245 + Map<String, Object> result = new LinkedHashMap<>();
  246 + result.put("records", records);
  247 + result.put("total", total);
  248 + result.put("pageNum", p);
  249 + result.put("pageSize", size);
  250 + return result;
  251 + }
  252 +
  253 + private Integer parseBoolean(String v) {
  254 + return switch (v.toLowerCase()) {
  255 + case "true", "1" -> 1;
  256 + case "false", "0" -> 0;
  257 + default -> -1;
  258 + };
  259 + }
174 } 260 }
backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java
@@ -28,7 +28,9 @@ import java.util.Map; @@ -28,7 +28,9 @@ import java.util.Map;
28 import static org.assertj.core.api.Assertions.assertThat; 28 import static org.assertj.core.api.Assertions.assertThat;
29 import static org.assertj.core.api.Assertions.assertThatThrownBy; 29 import static org.assertj.core.api.Assertions.assertThatThrownBy;
30 import static org.mockito.ArgumentMatchers.any; 30 import static org.mockito.ArgumentMatchers.any;
  31 +import static org.mockito.ArgumentMatchers.anyInt;
31 import static org.mockito.ArgumentMatchers.anyList; 32 import static org.mockito.ArgumentMatchers.anyList;
  33 +import static org.mockito.ArgumentMatchers.eq;
32 import static org.mockito.Mockito.lenient; 34 import static org.mockito.Mockito.lenient;
33 import static org.mockito.Mockito.mock; 35 import static org.mockito.Mockito.mock;
34 import static org.mockito.Mockito.never; 36 import static org.mockito.Mockito.never;
@@ -316,6 +318,95 @@ class UserServiceImplTest { @@ -316,6 +318,95 @@ class UserServiceImplTest {
316 verify(userPermissionMapper, never()).insert(any(UserPermission.class)); 318 verify(userPermissionMapper, never()).insert(any(UserPermission.class));
317 } 319 }
318 320
  321 + @Test
  322 + void listWithDefaults_invokesMapperWithUserNameContainsEmpty() {
  323 + when(userMapper.pageWithFilter(eq("u.sUserName"), eq("contains"), eq(""), eq(0), eq(20)))
  324 + .thenReturn(List.of());
  325 + when(userMapper.countWithFilter(eq("u.sUserName"), eq("contains"), eq(""))).thenReturn(0L);
  326 +
  327 + Map<String, Object> r = service.list(null, null, null, null, null);
  328 +
  329 + assertThat(r).containsEntry("total", 0L).containsEntry("pageNum", 1).containsEntry("pageSize", 20);
  330 + }
  331 +
  332 + @Test
  333 + void listWithEmptyValue_skipsFilterCondition() {
  334 + when(userMapper.pageWithFilter(any(), any(), any(), anyInt(), anyInt())).thenReturn(List.of());
  335 + when(userMapper.countWithFilter(any(), any(), any())).thenReturn(0L);
  336 +
  337 + service.list("用户号", "等于", "", 1, 20);
  338 +
  339 + ArgumentCaptor<Object> valueCap = ArgumentCaptor.forClass(Object.class);
  340 + verify(userMapper).pageWithFilter(eq("u.sUserNo"), eq("equals"), valueCap.capture(), eq(0), eq(20));
  341 + assertThat(valueCap.getValue()).isEqualTo("");
  342 + }
  343 +
  344 + @Test
  345 + void listWithKeywordTrim() {
  346 + when(userMapper.pageWithFilter(any(), any(), any(), anyInt(), anyInt())).thenReturn(List.of());
  347 + when(userMapper.countWithFilter(any(), any(), any())).thenReturn(0L);
  348 +
  349 + service.list("用户名", "包含", " abc ", 1, 10);
  350 +
  351 + verify(userMapper).pageWithFilter(eq("u.sUserName"), eq("contains"), eq("abc"), eq(0), eq(10));
  352 + }
  353 +
  354 + @Test
  355 + void listReturnsEmptyRecords_whenMapperReturnsEmptyPage() {
  356 + when(userMapper.pageWithFilter(any(), any(), any(), anyInt(), anyInt())).thenReturn(List.of());
  357 + when(userMapper.countWithFilter(any(), any(), any())).thenReturn(0L);
  358 +
  359 + Map<String, Object> r = service.list("用户名", "包含", "xyz", 1, 20);
  360 +
  361 + assertThat((List<?>) r.get("records")).isEmpty();
  362 + assertThat(r).containsEntry("total", 0L);
  363 + }
  364 +
  365 + @Test
  366 + void listWithInvalidField_throws40001() {
  367 + assertThatThrownBy(() -> service.list("未知字段", "包含", "x", 1, 20))
  368 + .isInstanceOf(BizException.class)
  369 + .hasFieldOrPropertyWithValue("code", 40001);
  370 + }
  371 +
  372 + @Test
  373 + void listWithInvalidMatch_throws40001() {
  374 + assertThatThrownBy(() -> service.list("用户名", "未知方式", "x", 1, 20))
  375 + .isInstanceOf(BizException.class)
  376 + .hasFieldOrPropertyWithValue("code", 40001);
  377 + }
  378 +
  379 + @Test
  380 + void listWithIncompatibleFieldMatch_throws40001() {
  381 + assertThatThrownBy(() -> service.list("作废", "包含", "true", 1, 20))
  382 + .isInstanceOf(BizException.class)
  383 + .hasFieldOrPropertyWithValue("code", 40001);
  384 + }
  385 +
  386 + @Test
  387 + void listWithPageSizeExceeds100_throws40002() {
  388 + assertThatThrownBy(() -> service.list("用户名", "包含", "", 1, 101))
  389 + .isInstanceOf(BizException.class)
  390 + .hasFieldOrPropertyWithValue("code", 40002);
  391 + }
  392 +
  393 + @Test
  394 + void listWithInvalidLoginDateFormat_throws40001() {
  395 + assertThatThrownBy(() -> service.list("登录日期", "等于", "abc", 1, 20))
  396 + .isInstanceOf(BizException.class)
  397 + .hasFieldOrPropertyWithValue("code", 40001);
  398 + }
  399 +
  400 + @Test
  401 + void listWithBooleanFieldEqualsTrue_passesIntegerOne() {
  402 + when(userMapper.pageWithFilter(any(), any(), any(), anyInt(), anyInt())).thenReturn(List.of());
  403 + when(userMapper.countWithFilter(any(), any(), any())).thenReturn(0L);
  404 +
  405 + service.list("作废", "等于", "true", 1, 20);
  406 +
  407 + verify(userMapper).pageWithFilter(eq("u.bDeleted"), eq("equals"), eq(1), eq(0), eq(20));
  408 + }
  409 +
319 private UpdateUserDTO baseUpdateDto() { 410 private UpdateUserDTO baseUpdateDto() {
320 UpdateUserDTO dto = new UpdateUserDTO(); 411 UpdateUserDTO dto = new UpdateUserDTO();
321 dto.setSUserNo("u_new"); 412 dto.setSUserNo("u_new");