Commit 0caab5b6c860982320fd077c29c1620e8ba6938d
1 parent
95bdce36
feat(usr): 查询用户 Service 参数判定与条件解析分页装配 REQ-USR-003
Showing
3 changed files
with
330 additions
and
1 deletions
backend/src/main/java/com/xly/erp/modules/usr/service/UsrUserService.java
| 1 | 1 | package com.xly.erp.modules.usr.service; |
| 2 | 2 | |
| 3 | +import com.xly.erp.common.response.PageResult; | |
| 3 | 4 | import com.xly.erp.modules.usr.dto.CreateUserDTO; |
| 4 | 5 | import com.xly.erp.modules.usr.dto.UpdateUserDTO; |
| 6 | +import com.xly.erp.modules.usr.dto.UserQueryDTO; | |
| 7 | +import com.xly.erp.modules.usr.vo.UserVO; | |
| 5 | 8 | |
| 6 | 9 | /** |
| 7 | - * 用户业务服务(docs/04 § 1.2)。REQ-USR-001 / REQ-USR-002。 | |
| 10 | + * 用户业务服务(docs/04 § 1.2)。REQ-USR-001 / REQ-USR-002 / REQ-USR-003。 | |
| 8 | 11 | */ |
| 9 | 12 | public interface UsrUserService { |
| 10 | 13 | |
| ... | ... | @@ -28,4 +31,15 @@ public interface UsrUserService { |
| 28 | 31 | * @return 被修改用户主键 iIncrement(等于入参 id) |
| 29 | 32 | */ |
| 30 | 33 | Integer updateUser(Integer id, UpdateUserDTO dto); |
| 34 | + | |
| 35 | + /** | |
| 36 | + * 查询用户(REQ-USR-003,纯只读):分页参数判定(42201)→ 单字段单匹配条件解析 | |
| 37 | + * (文本转义 / 枚举 / 布尔归一化 / 日期区间,非法 40001)→ {@code usr_user LEFT JOIN | |
| 38 | + * usr_employee} 分页查询 → 数据越界钳制到最后一页 → 装配 {@link PageResult}。 | |
| 39 | + * 空条件返回全量分页;响应不含密码 / 租户列。 | |
| 40 | + * | |
| 41 | + * @param dto 查询入参(全部可选) | |
| 42 | + * @return 分页结果 {@code PageResult<UserVO>} | |
| 43 | + */ | |
| 44 | + PageResult<UserVO> queryUsers(UserQueryDTO dto); | |
| 31 | 45 | } | ... | ... |
backend/src/main/java/com/xly/erp/modules/usr/service/impl/UsrUserServiceImpl.java
| 1 | 1 | package com.xly.erp.modules.usr.service.impl; |
| 2 | 2 | |
| 3 | +import com.baomidou.mybatisplus.core.metadata.IPage; | |
| 3 | 4 | import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
| 5 | +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | |
| 4 | 6 | import com.xly.erp.common.exception.BusinessException; |
| 7 | +import com.xly.erp.common.response.PageResult; | |
| 5 | 8 | import com.xly.erp.common.response.ResultCode; |
| 6 | 9 | import com.xly.erp.common.security.SecurityUtil; |
| 7 | 10 | import com.xly.erp.modules.usr.dto.CreateUserDTO; |
| 8 | 11 | import com.xly.erp.modules.usr.dto.UpdateUserDTO; |
| 12 | +import com.xly.erp.modules.usr.dto.UserQueryCondition; | |
| 13 | +import com.xly.erp.modules.usr.dto.UserQueryDTO; | |
| 9 | 14 | import com.xly.erp.modules.usr.entity.UsrUser; |
| 10 | 15 | import com.xly.erp.modules.usr.entity.UsrUserPermission; |
| 11 | 16 | import com.xly.erp.modules.usr.mapper.UsrEmployeeMapper; |
| ... | ... | @@ -13,8 +18,15 @@ import com.xly.erp.modules.usr.mapper.UsrPermissionMapper; |
| 13 | 18 | import com.xly.erp.modules.usr.mapper.UsrUserMapper; |
| 14 | 19 | import com.xly.erp.modules.usr.mapper.UsrUserPermissionMapper; |
| 15 | 20 | import com.xly.erp.modules.usr.service.UsrUserService; |
| 21 | +import com.xly.erp.modules.usr.vo.UserVO; | |
| 22 | +import java.time.LocalDate; | |
| 23 | +import java.time.LocalDateTime; | |
| 24 | +import java.time.LocalTime; | |
| 25 | +import java.time.format.DateTimeFormatter; | |
| 26 | +import java.time.format.DateTimeParseException; | |
| 16 | 27 | import java.util.LinkedHashSet; |
| 17 | 28 | import java.util.List; |
| 29 | +import java.util.Map; | |
| 18 | 30 | import org.springframework.dao.DuplicateKeyException; |
| 19 | 31 | import org.springframework.security.crypto.password.PasswordEncoder; |
| 20 | 32 | import org.springframework.stereotype.Service; |
| ... | ... | @@ -40,6 +52,39 @@ public class UsrUserServiceImpl implements UsrUserService { |
| 40 | 52 | /** 新建即生效。 */ |
| 41 | 53 | private static final int NOT_VOID = 0; |
| 42 | 54 | |
| 55 | + // ---- REQ-USR-003 查询用户:常量 / 白名单 ---- | |
| 56 | + | |
| 57 | + /** 默认查询字段(spec § 2.1)。 */ | |
| 58 | + private static final String DEFAULT_QUERY_FIELD = "用户名"; | |
| 59 | + /** 默认匹配方式(spec § 2.1)。 */ | |
| 60 | + private static final String DEFAULT_MATCH_TYPE = "包含"; | |
| 61 | + /** 默认页码。 */ | |
| 62 | + private static final long DEFAULT_PAGE_NUM = 1L; | |
| 63 | + /** 默认每页条数。 */ | |
| 64 | + private static final long DEFAULT_PAGE_SIZE = 10L; | |
| 65 | + /** 每页条数上限(spec § 3.7)。 */ | |
| 66 | + private static final long MAX_PAGE_SIZE = 100L; | |
| 67 | + | |
| 68 | + /** 中文 queryField → 白名单列 token(带表别名,绝不拼接用户输入列名,防注入;spec § 3.4)。 */ | |
| 69 | + private static final Map<String, String> FIELD_COLUMN = Map.of( | |
| 70 | + "用户名", "u.sUserName", | |
| 71 | + "员工名", "e.sEmployeeName", | |
| 72 | + "用户号", "u.sUserNo", | |
| 73 | + "部门", "e.sDepartment", | |
| 74 | + "用户类型", "u.sUserType", | |
| 75 | + "作废", "u.iIsVoid", | |
| 76 | + "登录日期", "u.tLastLoginDate", | |
| 77 | + "制单人", "u.sCreator"); | |
| 78 | + | |
| 79 | + /** 布尔字段集合(按 0/1 归一化)。 */ | |
| 80 | + private static final String FIELD_VOID = "作废"; | |
| 81 | + /** 日期字段集合(按当日区间解析)。 */ | |
| 82 | + private static final String FIELD_LOGIN_DATE = "登录日期"; | |
| 83 | + | |
| 84 | + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); | |
| 85 | + private static final DateTimeFormatter DATETIME_FMT = | |
| 86 | + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); | |
| 87 | + | |
| 43 | 88 | private final UsrUserMapper usrUserMapper; |
| 44 | 89 | private final UsrUserPermissionMapper usrUserPermissionMapper; |
| 45 | 90 | private final UsrEmployeeMapper usrEmployeeMapper; |
| ... | ... | @@ -192,4 +237,102 @@ public class UsrUserServiceImpl implements UsrUserService { |
| 192 | 237 | |
| 193 | 238 | return id; |
| 194 | 239 | } |
| 240 | + | |
| 241 | + @Override | |
| 242 | + @Transactional(readOnly = true) | |
| 243 | + public PageResult<UserVO> queryUsers(UserQueryDTO dto) { | |
| 244 | + // 1. 分页参数判定(先于一切;非法 → 42201,spec § 8 D8)。 | |
| 245 | + long pageNum = dto.getPageNum() != null ? dto.getPageNum() : DEFAULT_PAGE_NUM; | |
| 246 | + long pageSize = dto.getPageSize() != null ? dto.getPageSize() : DEFAULT_PAGE_SIZE; | |
| 247 | + if (pageNum < 1 || pageSize < 1 || pageSize > MAX_PAGE_SIZE) { | |
| 248 | + throw new BusinessException(ResultCode.PAGE_PARAM_INVALID); | |
| 249 | + } | |
| 250 | + | |
| 251 | + // 2. 条件解析(单字段单匹配;空值不施加过滤)。 | |
| 252 | + UserQueryCondition cond = parseCondition(dto); | |
| 253 | + | |
| 254 | + // 3. 分页查询。 | |
| 255 | + IPage<UserVO> page = usrUserMapper.selectUserPage(new Page<>(pageNum, pageSize), cond); | |
| 256 | + long total = page.getTotal(); | |
| 257 | + | |
| 258 | + // 4. 数据越界钳制到最后一页(spec § 3.7)。 | |
| 259 | + long pages = total == 0 ? 1 : (total + pageSize - 1) / pageSize; | |
| 260 | + if (total == 0) { | |
| 261 | + return PageResult.of(page.getRecords(), 0L, DEFAULT_PAGE_NUM, pageSize); | |
| 262 | + } | |
| 263 | + if (pageNum > pages) { | |
| 264 | + // 用钳后页号回查一次,保证 records 为最后一页真实数据。 | |
| 265 | + page = usrUserMapper.selectUserPage(new Page<>(pages, pageSize), cond); | |
| 266 | + return PageResult.of(page.getRecords(), total, pages, pageSize); | |
| 267 | + } | |
| 268 | + return PageResult.of(page.getRecords(), total, pageNum, pageSize); | |
| 269 | + } | |
| 270 | + | |
| 271 | + /** | |
| 272 | + * 把 DTO 的中文 queryField / matchType / 原始 queryValue 解析归一为 Mapper 可直接拼 XML 的条件。 | |
| 273 | + * queryValue 为 null / trim 后空 → 无过滤;按字段类型分文本 / 枚举 / 布尔 / 日期解析(非法 → 40001)。 | |
| 274 | + */ | |
| 275 | + private UserQueryCondition parseCondition(UserQueryDTO dto) { | |
| 276 | + String value = dto.getQueryValue(); | |
| 277 | + if (value == null || value.trim().isEmpty()) { | |
| 278 | + return UserQueryCondition.none(); | |
| 279 | + } | |
| 280 | + String field = StringUtils.hasText(dto.getQueryField()) ? dto.getQueryField() : DEFAULT_QUERY_FIELD; | |
| 281 | + String matchType = StringUtils.hasText(dto.getMatchType()) ? dto.getMatchType() : DEFAULT_MATCH_TYPE; | |
| 282 | + String column = FIELD_COLUMN.get(field); | |
| 283 | + if (column == null) { | |
| 284 | + throw new BusinessException(ResultCode.PARAM_INVALID, "查询字段取值非法"); | |
| 285 | + } | |
| 286 | + String trimmed = value.trim(); | |
| 287 | + | |
| 288 | + if (FIELD_VOID.equals(field)) { | |
| 289 | + int boolValue = normalizeBool(trimmed); | |
| 290 | + boolean negated = "不包含".equals(matchType); | |
| 291 | + return UserQueryCondition.bool(column, boolValue, negated); | |
| 292 | + } | |
| 293 | + if (FIELD_LOGIN_DATE.equals(field)) { | |
| 294 | + LocalDateTime start = parseDayStart(trimmed); | |
| 295 | + LocalDateTime end = start.toLocalDate().plusDays(1).atStartOfDay(); | |
| 296 | + boolean negated = "不包含".equals(matchType); | |
| 297 | + return UserQueryCondition.date(column, start, end, negated); | |
| 298 | + } | |
| 299 | + // 文本 / 枚举:等于走 = 不转义;包含 / 不包含走 LIKE,对 % _ \ 转义。 | |
| 300 | + if ("等于".equals(matchType)) { | |
| 301 | + return UserQueryCondition.text(column, matchType, trimmed); | |
| 302 | + } | |
| 303 | + return UserQueryCondition.text(column, matchType, escapeLike(trimmed)); | |
| 304 | + } | |
| 305 | + | |
| 306 | + /** 布尔归一化:接受 0/1、是/否、true/false(spec § 8 D6);不可解析 → 40001。 */ | |
| 307 | + private int normalizeBool(String value) { | |
| 308 | + String v = value.trim().toLowerCase(); | |
| 309 | + if ("1".equals(v) || "是".equals(value.trim()) || "true".equals(v)) { | |
| 310 | + return 1; | |
| 311 | + } | |
| 312 | + if ("0".equals(v) || "否".equals(value.trim()) || "false".equals(v)) { | |
| 313 | + return 0; | |
| 314 | + } | |
| 315 | + throw new BusinessException(ResultCode.PARAM_INVALID, "作废取值不可解析"); | |
| 316 | + } | |
| 317 | + | |
| 318 | + /** 日期解析为当日起点:支持 yyyy-MM-dd 与 yyyy-MM-dd HH:mm:ss(spec § 8 D6);非法 → 40001。 */ | |
| 319 | + private LocalDateTime parseDayStart(String value) { | |
| 320 | + try { | |
| 321 | + if (value.length() > 10) { | |
| 322 | + LocalDateTime dt = LocalDateTime.parse(value, DATETIME_FMT); | |
| 323 | + return dt.toLocalDate().atStartOfDay(); | |
| 324 | + } | |
| 325 | + LocalDate d = LocalDate.parse(value, DATE_FMT); | |
| 326 | + return d.atTime(LocalTime.MIDNIGHT); | |
| 327 | + } catch (DateTimeParseException ex) { | |
| 328 | + throw new BusinessException(ResultCode.PARAM_INVALID, "登录日期取值非法"); | |
| 329 | + } | |
| 330 | + } | |
| 331 | + | |
| 332 | + /** LIKE 通配符转义:对 \ % _ 前置反斜杠(配合 XML 的 ESCAPE '\\',spec § 8 D3)。 */ | |
| 333 | + private String escapeLike(String value) { | |
| 334 | + return value.replace("\\", "\\\\") | |
| 335 | + .replace("%", "\\%") | |
| 336 | + .replace("_", "\\_"); | |
| 337 | + } | |
| 195 | 338 | } | ... | ... |
backend/src/test/java/com/xly/erp/modules/usr/service/UsrUserServiceImplTest.java
| ... | ... | @@ -10,11 +10,18 @@ import static org.mockito.Mockito.verify; |
| 10 | 10 | import static org.mockito.Mockito.when; |
| 11 | 11 | |
| 12 | 12 | import com.baomidou.mybatisplus.core.conditions.Wrapper; |
| 13 | +import com.baomidou.mybatisplus.core.metadata.IPage; | |
| 14 | +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | |
| 13 | 15 | import com.xly.erp.common.exception.BusinessException; |
| 16 | +import com.xly.erp.common.response.PageResult; | |
| 14 | 17 | import com.xly.erp.common.response.ResultCode; |
| 15 | 18 | import com.xly.erp.common.security.SecurityUtil; |
| 16 | 19 | import com.xly.erp.modules.usr.dto.CreateUserDTO; |
| 17 | 20 | import com.xly.erp.modules.usr.dto.UpdateUserDTO; |
| 21 | +import com.xly.erp.modules.usr.dto.UserQueryCondition; | |
| 22 | +import com.xly.erp.modules.usr.dto.UserQueryDTO; | |
| 23 | +import com.xly.erp.modules.usr.vo.UserVO; | |
| 24 | +import java.time.LocalDateTime; | |
| 18 | 25 | import com.xly.erp.modules.usr.entity.UsrEmployee; |
| 19 | 26 | import com.xly.erp.modules.usr.entity.UsrPermission; |
| 20 | 27 | import com.xly.erp.modules.usr.entity.UsrUser; |
| ... | ... | @@ -357,4 +364,169 @@ class UsrUserServiceImplTest { |
| 357 | 364 | verify(usrUserPermissionMapper, never()).delete(any(Wrapper.class)); |
| 358 | 365 | verify(usrUserPermissionMapper, never()).insert(any(UsrUserPermission.class)); |
| 359 | 366 | } |
| 367 | + | |
| 368 | + // ---------------- REQ-USR-003 T4:查询用户 参数判定 + 条件解析 + 分页装配 ---------------- | |
| 369 | + | |
| 370 | + /** 桩:selectUserPage 返回携带指定 records/total/size/current 的 Page。 */ | |
| 371 | + @SuppressWarnings("unchecked") | |
| 372 | + private void stubSelectUserPage(java.util.List<UserVO> records, long total, long size, long current) { | |
| 373 | + when(usrUserMapper.selectUserPage(any(IPage.class), any(UserQueryCondition.class))) | |
| 374 | + .thenAnswer(inv -> { | |
| 375 | + Page<UserVO> page = new Page<>(current, size); | |
| 376 | + page.setRecords(records); | |
| 377 | + page.setTotal(total); | |
| 378 | + return page; | |
| 379 | + }); | |
| 380 | + } | |
| 381 | + | |
| 382 | + private UserQueryDTO queryDto(String field, String matchType, String value) { | |
| 383 | + UserQueryDTO dto = new UserQueryDTO(); | |
| 384 | + dto.setQueryField(field); | |
| 385 | + dto.setMatchType(matchType); | |
| 386 | + dto.setQueryValue(value); | |
| 387 | + return dto; | |
| 388 | + } | |
| 389 | + | |
| 390 | + @Test | |
| 391 | + @SuppressWarnings("unchecked") | |
| 392 | + void pageParamTooSmallThrows42201() { | |
| 393 | + UserQueryDTO dto = new UserQueryDTO(); | |
| 394 | + dto.setPageNum(0); | |
| 395 | + assertThatThrownBy(() -> service.queryUsers(dto)) | |
| 396 | + .isInstanceOf(BusinessException.class) | |
| 397 | + .extracting(e -> ((BusinessException) e).getResultCode()) | |
| 398 | + .isEqualTo(ResultCode.PAGE_PARAM_INVALID); | |
| 399 | + | |
| 400 | + UserQueryDTO dto2 = new UserQueryDTO(); | |
| 401 | + dto2.setPageSize(0); | |
| 402 | + assertThatThrownBy(() -> service.queryUsers(dto2)) | |
| 403 | + .isInstanceOf(BusinessException.class) | |
| 404 | + .extracting(e -> ((BusinessException) e).getResultCode()) | |
| 405 | + .isEqualTo(ResultCode.PAGE_PARAM_INVALID); | |
| 406 | + | |
| 407 | + verify(usrUserMapper, never()).selectUserPage(any(IPage.class), any(UserQueryCondition.class)); | |
| 408 | + } | |
| 409 | + | |
| 410 | + @Test | |
| 411 | + @SuppressWarnings("unchecked") | |
| 412 | + void pageSizeOverMaxThrows42201() { | |
| 413 | + UserQueryDTO dto = new UserQueryDTO(); | |
| 414 | + dto.setPageSize(500); | |
| 415 | + assertThatThrownBy(() -> service.queryUsers(dto)) | |
| 416 | + .isInstanceOf(BusinessException.class) | |
| 417 | + .extracting(e -> ((BusinessException) e).getResultCode()) | |
| 418 | + .isEqualTo(ResultCode.PAGE_PARAM_INVALID); | |
| 419 | + verify(usrUserMapper, never()).selectUserPage(any(IPage.class), any(UserQueryCondition.class)); | |
| 420 | + } | |
| 421 | + | |
| 422 | + @Test | |
| 423 | + @SuppressWarnings("unchecked") | |
| 424 | + void blankQueryValueAppliesNoFilter() { | |
| 425 | + stubSelectUserPage(java.util.List.of(), 0L, 10L, 1L); | |
| 426 | + UserQueryDTO dto = queryDto("用户名", "包含", null); | |
| 427 | + | |
| 428 | + service.queryUsers(dto); | |
| 429 | + | |
| 430 | + ArgumentCaptor<UserQueryCondition> captor = ArgumentCaptor.forClass(UserQueryCondition.class); | |
| 431 | + verify(usrUserMapper).selectUserPage(any(IPage.class), captor.capture()); | |
| 432 | + UserQueryCondition cond = captor.getValue(); | |
| 433 | + assertThat(cond.getKind()).isEqualTo(UserQueryCondition.Kind.NONE); | |
| 434 | + assertThat(cond.getTextValue()).isNull(); | |
| 435 | + } | |
| 436 | + | |
| 437 | + @Test | |
| 438 | + @SuppressWarnings("unchecked") | |
| 439 | + void boolFieldUnparsableThrows40001() { | |
| 440 | + UserQueryDTO bad = queryDto("作废", "等于", "abc"); | |
| 441 | + assertThatThrownBy(() -> service.queryUsers(bad)) | |
| 442 | + .isInstanceOf(BusinessException.class) | |
| 443 | + .extracting(e -> ((BusinessException) e).getResultCode()) | |
| 444 | + .isEqualTo(ResultCode.PARAM_INVALID); | |
| 445 | + verify(usrUserMapper, never()).selectUserPage(any(IPage.class), any(UserQueryCondition.class)); | |
| 446 | + | |
| 447 | + stubSelectUserPage(java.util.List.of(), 0L, 10L, 1L); | |
| 448 | + ArgumentCaptor<UserQueryCondition> captor = ArgumentCaptor.forClass(UserQueryCondition.class); | |
| 449 | + | |
| 450 | + service.queryUsers(queryDto("作废", "等于", "1")); | |
| 451 | + verify(usrUserMapper).selectUserPage(any(IPage.class), captor.capture()); | |
| 452 | + assertThat(captor.getValue().getBoolValue()).isEqualTo(1); | |
| 453 | + | |
| 454 | + service.queryUsers(queryDto("作废", "等于", "是")); | |
| 455 | + verify(usrUserMapper, times(2)).selectUserPage(any(IPage.class), captor.capture()); | |
| 456 | + assertThat(captor.getValue().getBoolValue()).isEqualTo(1); | |
| 457 | + } | |
| 458 | + | |
| 459 | + @Test | |
| 460 | + @SuppressWarnings("unchecked") | |
| 461 | + void dateFieldIllegalThrows40001() { | |
| 462 | + UserQueryDTO bad = queryDto("登录日期", "等于", "2026-13-99"); | |
| 463 | + assertThatThrownBy(() -> service.queryUsers(bad)) | |
| 464 | + .isInstanceOf(BusinessException.class) | |
| 465 | + .extracting(e -> ((BusinessException) e).getResultCode()) | |
| 466 | + .isEqualTo(ResultCode.PARAM_INVALID); | |
| 467 | + verify(usrUserMapper, never()).selectUserPage(any(IPage.class), any(UserQueryCondition.class)); | |
| 468 | + | |
| 469 | + stubSelectUserPage(java.util.List.of(), 0L, 10L, 1L); | |
| 470 | + service.queryUsers(queryDto("登录日期", "等于", "2026-06-01")); | |
| 471 | + ArgumentCaptor<UserQueryCondition> captor = ArgumentCaptor.forClass(UserQueryCondition.class); | |
| 472 | + verify(usrUserMapper).selectUserPage(any(IPage.class), captor.capture()); | |
| 473 | + UserQueryCondition cond = captor.getValue(); | |
| 474 | + assertThat(cond.getDateStart()).isEqualTo(LocalDateTime.of(2026, 6, 1, 0, 0, 0)); | |
| 475 | + assertThat(cond.getDateEnd()).isEqualTo(LocalDateTime.of(2026, 6, 2, 0, 0, 0)); | |
| 476 | + } | |
| 477 | + | |
| 478 | + @Test | |
| 479 | + @SuppressWarnings("unchecked") | |
| 480 | + void textLikeEscapesWildcards() { | |
| 481 | + stubSelectUserPage(java.util.List.of(), 0L, 10L, 1L); | |
| 482 | + service.queryUsers(queryDto("用户名", "包含", "a%_b")); | |
| 483 | + | |
| 484 | + ArgumentCaptor<UserQueryCondition> captor = ArgumentCaptor.forClass(UserQueryCondition.class); | |
| 485 | + verify(usrUserMapper).selectUserPage(any(IPage.class), captor.capture()); | |
| 486 | + UserQueryCondition cond = captor.getValue(); | |
| 487 | + assertThat(cond.isText()).isTrue(); | |
| 488 | + assertThat(cond.isTextContains()).isTrue(); | |
| 489 | + assertThat(cond.getTextValue()).contains("\\%").contains("\\_"); | |
| 490 | + } | |
| 491 | + | |
| 492 | + @Test | |
| 493 | + @SuppressWarnings("unchecked") | |
| 494 | + void dataPageOutOfRangeClampsToLastPage() { | |
| 495 | + UserVO vo = new UserVO(); | |
| 496 | + vo.setSUserName("last_page_user"); | |
| 497 | + stubSelectUserPage(java.util.List.of(vo), 23L, 10L, 99L); | |
| 498 | + UserQueryDTO dto = new UserQueryDTO(); | |
| 499 | + dto.setPageNum(99); | |
| 500 | + dto.setPageSize(10); | |
| 501 | + | |
| 502 | + PageResult<UserVO> result = service.queryUsers(dto); | |
| 503 | + | |
| 504 | + assertThat(result.getTotal()).isEqualTo(23L); | |
| 505 | + assertThat(result.getPageNum()).isEqualTo(3L); | |
| 506 | + assertThat(result.getRecords()).extracting(UserVO::getSUserName).contains("last_page_user"); | |
| 507 | + } | |
| 508 | + | |
| 509 | + @Test | |
| 510 | + @SuppressWarnings("unchecked") | |
| 511 | + void emptyResultReturnsZeroTotal() { | |
| 512 | + stubSelectUserPage(java.util.List.of(), 0L, 10L, 1L); | |
| 513 | + PageResult<UserVO> result = service.queryUsers(new UserQueryDTO()); | |
| 514 | + | |
| 515 | + assertThat(result.getRecords()).isEmpty(); | |
| 516 | + assertThat(result.getTotal()).isZero(); | |
| 517 | + assertThat(result.getPageNum()).isEqualTo(1L); | |
| 518 | + } | |
| 519 | + | |
| 520 | + @Test | |
| 521 | + @SuppressWarnings("unchecked") | |
| 522 | + void defaultsApplied() { | |
| 523 | + stubSelectUserPage(java.util.List.of(), 0L, 10L, 1L); | |
| 524 | + service.queryUsers(new UserQueryDTO()); | |
| 525 | + | |
| 526 | + ArgumentCaptor<IPage> pageCaptor = ArgumentCaptor.forClass(IPage.class); | |
| 527 | + verify(usrUserMapper).selectUserPage(pageCaptor.capture(), any(UserQueryCondition.class)); | |
| 528 | + IPage<?> page = pageCaptor.getValue(); | |
| 529 | + assertThat(page.getCurrent()).isEqualTo(1L); | |
| 530 | + assertThat(page.getSize()).isEqualTo(10L); | |
| 531 | + } | |
| 360 | 532 | } | ... | ... |