Commit 57a1bbfc7d7a7fdd4e68df6f8d16006fdff3b6c6
1 parent
9895c567
feat(usr): user list service + field/match mapping REQ-USR-003
Showing
3 changed files
with
179 additions
and
0 deletions
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"); |