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 | package com.xly.erp.modules.usr.service; | 1 | package com.xly.erp.modules.usr.service; |
| 2 | 2 | ||
| 3 | +import com.xly.erp.common.response.PageResult; | ||
| 3 | import com.xly.erp.modules.usr.dto.CreateUserDTO; | 4 | import com.xly.erp.modules.usr.dto.CreateUserDTO; |
| 4 | import com.xly.erp.modules.usr.dto.UpdateUserDTO; | 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 | public interface UsrUserService { | 12 | public interface UsrUserService { |
| 10 | 13 | ||
| @@ -28,4 +31,15 @@ public interface UsrUserService { | @@ -28,4 +31,15 @@ public interface UsrUserService { | ||
| 28 | * @return 被修改用户主键 iIncrement(等于入参 id) | 31 | * @return 被修改用户主键 iIncrement(等于入参 id) |
| 29 | */ | 32 | */ |
| 30 | Integer updateUser(Integer id, UpdateUserDTO dto); | 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 | package com.xly.erp.modules.usr.service.impl; | 1 | package com.xly.erp.modules.usr.service.impl; |
| 2 | 2 | ||
| 3 | +import com.baomidou.mybatisplus.core.metadata.IPage; | ||
| 3 | import com.baomidou.mybatisplus.core.toolkit.Wrappers; | 4 | import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
| 5 | +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | ||
| 4 | import com.xly.erp.common.exception.BusinessException; | 6 | import com.xly.erp.common.exception.BusinessException; |
| 7 | +import com.xly.erp.common.response.PageResult; | ||
| 5 | import com.xly.erp.common.response.ResultCode; | 8 | import com.xly.erp.common.response.ResultCode; |
| 6 | import com.xly.erp.common.security.SecurityUtil; | 9 | import com.xly.erp.common.security.SecurityUtil; |
| 7 | import com.xly.erp.modules.usr.dto.CreateUserDTO; | 10 | import com.xly.erp.modules.usr.dto.CreateUserDTO; |
| 8 | import com.xly.erp.modules.usr.dto.UpdateUserDTO; | 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 | import com.xly.erp.modules.usr.entity.UsrUser; | 14 | import com.xly.erp.modules.usr.entity.UsrUser; |
| 10 | import com.xly.erp.modules.usr.entity.UsrUserPermission; | 15 | import com.xly.erp.modules.usr.entity.UsrUserPermission; |
| 11 | import com.xly.erp.modules.usr.mapper.UsrEmployeeMapper; | 16 | import com.xly.erp.modules.usr.mapper.UsrEmployeeMapper; |
| @@ -13,8 +18,15 @@ import com.xly.erp.modules.usr.mapper.UsrPermissionMapper; | @@ -13,8 +18,15 @@ import com.xly.erp.modules.usr.mapper.UsrPermissionMapper; | ||
| 13 | import com.xly.erp.modules.usr.mapper.UsrUserMapper; | 18 | import com.xly.erp.modules.usr.mapper.UsrUserMapper; |
| 14 | import com.xly.erp.modules.usr.mapper.UsrUserPermissionMapper; | 19 | import com.xly.erp.modules.usr.mapper.UsrUserPermissionMapper; |
| 15 | import com.xly.erp.modules.usr.service.UsrUserService; | 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 | import java.util.LinkedHashSet; | 27 | import java.util.LinkedHashSet; |
| 17 | import java.util.List; | 28 | import java.util.List; |
| 29 | +import java.util.Map; | ||
| 18 | import org.springframework.dao.DuplicateKeyException; | 30 | import org.springframework.dao.DuplicateKeyException; |
| 19 | import org.springframework.security.crypto.password.PasswordEncoder; | 31 | import org.springframework.security.crypto.password.PasswordEncoder; |
| 20 | import org.springframework.stereotype.Service; | 32 | import org.springframework.stereotype.Service; |
| @@ -40,6 +52,39 @@ public class UsrUserServiceImpl implements UsrUserService { | @@ -40,6 +52,39 @@ public class UsrUserServiceImpl implements UsrUserService { | ||
| 40 | /** 新建即生效。 */ | 52 | /** 新建即生效。 */ |
| 41 | private static final int NOT_VOID = 0; | 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 | private final UsrUserMapper usrUserMapper; | 88 | private final UsrUserMapper usrUserMapper; |
| 44 | private final UsrUserPermissionMapper usrUserPermissionMapper; | 89 | private final UsrUserPermissionMapper usrUserPermissionMapper; |
| 45 | private final UsrEmployeeMapper usrEmployeeMapper; | 90 | private final UsrEmployeeMapper usrEmployeeMapper; |
| @@ -192,4 +237,102 @@ public class UsrUserServiceImpl implements UsrUserService { | @@ -192,4 +237,102 @@ public class UsrUserServiceImpl implements UsrUserService { | ||
| 192 | 237 | ||
| 193 | return id; | 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,11 +10,18 @@ import static org.mockito.Mockito.verify; | ||
| 10 | import static org.mockito.Mockito.when; | 10 | import static org.mockito.Mockito.when; |
| 11 | 11 | ||
| 12 | import com.baomidou.mybatisplus.core.conditions.Wrapper; | 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 | import com.xly.erp.common.exception.BusinessException; | 15 | import com.xly.erp.common.exception.BusinessException; |
| 16 | +import com.xly.erp.common.response.PageResult; | ||
| 14 | import com.xly.erp.common.response.ResultCode; | 17 | import com.xly.erp.common.response.ResultCode; |
| 15 | import com.xly.erp.common.security.SecurityUtil; | 18 | import com.xly.erp.common.security.SecurityUtil; |
| 16 | import com.xly.erp.modules.usr.dto.CreateUserDTO; | 19 | import com.xly.erp.modules.usr.dto.CreateUserDTO; |
| 17 | import com.xly.erp.modules.usr.dto.UpdateUserDTO; | 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 | import com.xly.erp.modules.usr.entity.UsrEmployee; | 25 | import com.xly.erp.modules.usr.entity.UsrEmployee; |
| 19 | import com.xly.erp.modules.usr.entity.UsrPermission; | 26 | import com.xly.erp.modules.usr.entity.UsrPermission; |
| 20 | import com.xly.erp.modules.usr.entity.UsrUser; | 27 | import com.xly.erp.modules.usr.entity.UsrUser; |
| @@ -357,4 +364,169 @@ class UsrUserServiceImplTest { | @@ -357,4 +364,169 @@ class UsrUserServiceImplTest { | ||
| 357 | verify(usrUserPermissionMapper, never()).delete(any(Wrapper.class)); | 364 | verify(usrUserPermissionMapper, never()).delete(any(Wrapper.class)); |
| 358 | verify(usrUserPermissionMapper, never()).insert(any(UsrUserPermission.class)); | 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 | } |