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
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 | 7 | import com.xly.erp.module.usr.dto.CreateUserDTO; |
| 8 | 8 | import com.xly.erp.module.usr.dto.UpdateUserDTO; |
| 9 | 9 | import com.xly.erp.module.usr.entity.User; |
| 10 | +import com.xly.erp.module.usr.vo.UserListVO; | |
| 10 | 11 | import com.xly.erp.module.usr.entity.UserPermission; |
| 11 | 12 | import com.xly.erp.module.usr.mapper.PermissionCategoryMapper; |
| 12 | 13 | import com.xly.erp.module.usr.mapper.StaffMapper; |
| ... | ... | @@ -18,7 +19,9 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
| 18 | 19 | import org.springframework.stereotype.Service; |
| 19 | 20 | import org.springframework.transaction.annotation.Transactional; |
| 20 | 21 | |
| 22 | +import java.time.LocalDate; | |
| 21 | 23 | import java.time.LocalDateTime; |
| 24 | +import java.time.format.DateTimeParseException; | |
| 22 | 25 | import java.util.LinkedHashMap; |
| 23 | 26 | import java.util.List; |
| 24 | 27 | import java.util.Map; |
| ... | ... | @@ -32,6 +35,30 @@ public class UserServiceImpl implements UserService { |
| 32 | 35 | static final Set<String> LANGUAGES = Set.of("zh", "en", "zh-TW"); |
| 33 | 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 | 62 | private final UserMapper userMapper; |
| 36 | 63 | private final UserPermissionMapper userPermissionMapper; |
| 37 | 64 | private final StaffMapper staffMapper; |
| ... | ... | @@ -171,4 +198,63 @@ public class UserServiceImpl implements UserService { |
| 171 | 198 | } |
| 172 | 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 | 28 | import static org.assertj.core.api.Assertions.assertThat; |
| 29 | 29 | import static org.assertj.core.api.Assertions.assertThatThrownBy; |
| 30 | 30 | import static org.mockito.ArgumentMatchers.any; |
| 31 | +import static org.mockito.ArgumentMatchers.anyInt; | |
| 31 | 32 | import static org.mockito.ArgumentMatchers.anyList; |
| 33 | +import static org.mockito.ArgumentMatchers.eq; | |
| 32 | 34 | import static org.mockito.Mockito.lenient; |
| 33 | 35 | import static org.mockito.Mockito.mock; |
| 34 | 36 | import static org.mockito.Mockito.never; |
| ... | ... | @@ -316,6 +318,95 @@ class UserServiceImplTest { |
| 316 | 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 | 410 | private UpdateUserDTO baseUpdateDto() { |
| 320 | 411 | UpdateUserDTO dto = new UpdateUserDTO(); |
| 321 | 412 | dto.setSUserNo("u_new"); | ... | ... |