Commit 3f1b9e89007a308b451ea4c6d30db4e9f8a78dda
1 parent
b4a152cc
feat(usr): update user service REQ-USR-002
Showing
3 changed files
with
252 additions
and
0 deletions
backend/src/main/java/com/xly/erp/module/usr/service/UserService.java
| 1 | package com.xly.erp.module.usr.service; | 1 | package com.xly.erp.module.usr.service; |
| 2 | 2 | ||
| 3 | import com.xly.erp.module.usr.dto.UserCreateDTO; | 3 | import com.xly.erp.module.usr.dto.UserCreateDTO; |
| 4 | +import com.xly.erp.module.usr.dto.UserUpdateDTO; | ||
| 4 | import com.xly.erp.module.usr.vo.UserVO; | 5 | import com.xly.erp.module.usr.vo.UserVO; |
| 5 | 6 | ||
| 6 | public interface UserService { | 7 | public interface UserService { |
| 7 | /** REQ-USR-001 用户新增 */ | 8 | /** REQ-USR-001 用户新增 */ |
| 8 | UserVO create(UserCreateDTO dto); | 9 | UserVO create(UserCreateDTO dto); |
| 10 | + | ||
| 11 | + /** REQ-USR-002 用户修改 */ | ||
| 12 | + UserVO update(Integer id, UserUpdateDTO dto); | ||
| 9 | } | 13 | } |
backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java
| @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | ||
| 4 | import com.xly.erp.common.exception.BizException; | 4 | import com.xly.erp.common.exception.BizException; |
| 5 | import com.xly.erp.common.response.ErrorCode; | 5 | import com.xly.erp.common.response.ErrorCode; |
| 6 | import com.xly.erp.module.usr.dto.UserCreateDTO; | 6 | import com.xly.erp.module.usr.dto.UserCreateDTO; |
| 7 | +import com.xly.erp.module.usr.dto.UserUpdateDTO; | ||
| 7 | import com.xly.erp.module.usr.entity.PermissionCategoryEntity; | 8 | import com.xly.erp.module.usr.entity.PermissionCategoryEntity; |
| 8 | import com.xly.erp.module.usr.entity.StaffEntity; | 9 | import com.xly.erp.module.usr.entity.StaffEntity; |
| 9 | import com.xly.erp.module.usr.entity.UserEntity; | 10 | import com.xly.erp.module.usr.entity.UserEntity; |
| @@ -106,4 +107,59 @@ public class UserServiceImpl implements UserService { | @@ -106,4 +107,59 @@ public class UserServiceImpl implements UserService { | ||
| 106 | 107 | ||
| 107 | return UserVO.from(user, categoryIds); | 108 | return UserVO.from(user, categoryIds); |
| 108 | } | 109 | } |
| 110 | + | ||
| 111 | + /** REQ-USR-002 用户修改 */ | ||
| 112 | + @Override | ||
| 113 | + @Transactional(rollbackFor = Exception.class) | ||
| 114 | + public UserVO update(Integer id, UserUpdateDTO dto) { | ||
| 115 | + // 1. 目标用户存在 + 未软删除 | ||
| 116 | + UserEntity target = userMapper.selectById(id); | ||
| 117 | + if (target == null || Boolean.TRUE.equals(target.getBDeleted())) { | ||
| 118 | + throw new BizException(ErrorCode.USR_NOT_FOUND); | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + // 2. iStaffId 校验(仅当非空) | ||
| 122 | + if (dto.getIStaffId() != null) { | ||
| 123 | + StaffEntity staff = staffMapper.selectById(dto.getIStaffId()); | ||
| 124 | + if (staff == null || Boolean.TRUE.equals(staff.getBDeleted())) { | ||
| 125 | + throw new BizException(ErrorCode.STAFF_NOT_FOUND); | ||
| 126 | + } | ||
| 127 | + } | ||
| 128 | + | ||
| 129 | + // 3. 权限分类校验(仅当非空) | ||
| 130 | + List<Integer> categoryIds = dto.getPermissionCategoryIds() == null | ||
| 131 | + ? new ArrayList<>() : dto.getPermissionCategoryIds(); | ||
| 132 | + if (!categoryIds.isEmpty()) { | ||
| 133 | + List<PermissionCategoryEntity> found = permissionCategoryMapper.selectBatchIds(categoryIds); | ||
| 134 | + if (found.size() != categoryIds.size() | ||
| 135 | + || found.stream().anyMatch(p -> Boolean.TRUE.equals(p.getBDeleted()))) { | ||
| 136 | + throw new BizException(ErrorCode.PERM_CATEGORY_NOT_FOUND); | ||
| 137 | + } | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + // 4. 字段合并到 target(保留 sUserNo / sUserName / sPasswordHash 等保护字段) | ||
| 141 | + target.setIStaffId(dto.getIStaffId()); // 含 null 清空(依赖 iStaffId.IGNORED 策略) | ||
| 142 | + target.setSUserType(dto.getSUserType()); | ||
| 143 | + target.setSLanguage(dto.getSLanguage()); | ||
| 144 | + if (dto.getBCanModifyDocs() != null) { | ||
| 145 | + target.setBCanModifyDocs(dto.getBCanModifyDocs()); | ||
| 146 | + } | ||
| 147 | + | ||
| 148 | + // 5. 落库 user | ||
| 149 | + userMapper.updateById(target); | ||
| 150 | + | ||
| 151 | + // 6. 重建权限关联:先删后插(清空原有,再按 dto 插入) | ||
| 152 | + userPermissionMapper.delete( | ||
| 153 | + new LambdaQueryWrapper<UserPermissionEntity>() | ||
| 154 | + .eq(UserPermissionEntity::getIUserId, id)); | ||
| 155 | + for (Integer categoryId : categoryIds) { | ||
| 156 | + UserPermissionEntity up = new UserPermissionEntity(); | ||
| 157 | + up.setIUserId(id); | ||
| 158 | + up.setICategoryId(categoryId); | ||
| 159 | + up.setTCreateDate(LocalDateTime.now()); | ||
| 160 | + userPermissionMapper.insert(up); | ||
| 161 | + } | ||
| 162 | + | ||
| 163 | + return UserVO.from(target, categoryIds); | ||
| 164 | + } | ||
| 109 | } | 165 | } |
backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java
| @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper; | @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper; | ||
| 4 | import com.xly.erp.common.exception.BizException; | 4 | import com.xly.erp.common.exception.BizException; |
| 5 | import com.xly.erp.common.response.ErrorCode; | 5 | import com.xly.erp.common.response.ErrorCode; |
| 6 | import com.xly.erp.module.usr.dto.UserCreateDTO; | 6 | import com.xly.erp.module.usr.dto.UserCreateDTO; |
| 7 | +import com.xly.erp.module.usr.dto.UserUpdateDTO; | ||
| 7 | import com.xly.erp.module.usr.entity.PermissionCategoryEntity; | 8 | import com.xly.erp.module.usr.entity.PermissionCategoryEntity; |
| 8 | import com.xly.erp.module.usr.entity.StaffEntity; | 9 | import com.xly.erp.module.usr.entity.StaffEntity; |
| 9 | import com.xly.erp.module.usr.entity.UserEntity; | 10 | import com.xly.erp.module.usr.entity.UserEntity; |
| @@ -218,4 +219,195 @@ class UserServiceImplTest { | @@ -218,4 +219,195 @@ class UserServiceImplTest { | ||
| 218 | p.setBDeleted(false); | 219 | p.setBDeleted(false); |
| 219 | return p; | 220 | return p; |
| 220 | } | 221 | } |
| 222 | + | ||
| 223 | + // ============================================================ | ||
| 224 | + // REQ-USR-002 update 系列 | ||
| 225 | + // ============================================================ | ||
| 226 | + | ||
| 227 | + private UserUpdateDTO updateDto() { | ||
| 228 | + UserUpdateDTO d = new UserUpdateDTO(); | ||
| 229 | + d.setIStaffId(7); | ||
| 230 | + d.setSUserType("超级管理员"); | ||
| 231 | + d.setSLanguage("en"); | ||
| 232 | + d.setBCanModifyDocs(true); | ||
| 233 | + d.setPermissionCategoryIds(List.of(10, 20)); | ||
| 234 | + return d; | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | + private UserEntity existingTargetUser(int id) { | ||
| 238 | + UserEntity u = new UserEntity(); | ||
| 239 | + u.setIIncrement(id); | ||
| 240 | + u.setSUserNo("u_orig"); | ||
| 241 | + u.setSUserName("alice_orig"); | ||
| 242 | + u.setIStaffId(3); | ||
| 243 | + u.setSUserType("普通用户"); | ||
| 244 | + u.setSLanguage("zh"); | ||
| 245 | + u.setBCanModifyDocs(false); | ||
| 246 | + u.setSPasswordHash("$2a$10$origHash"); | ||
| 247 | + u.setBDeleted(false); | ||
| 248 | + u.setSCreatedBy("system"); | ||
| 249 | + return u; | ||
| 250 | + } | ||
| 251 | + | ||
| 252 | + @Test | ||
| 253 | + void update_targetNotFound_throws40431() { | ||
| 254 | + when(userMapper.selectById(99)).thenReturn(null); | ||
| 255 | + | ||
| 256 | + assertThatThrownBy(() -> service.update(99, updateDto())) | ||
| 257 | + .isInstanceOf(BizException.class) | ||
| 258 | + .extracting(e -> ((BizException) e).getCode()) | ||
| 259 | + .isEqualTo(ErrorCode.USR_NOT_FOUND.getCode()); | ||
| 260 | + } | ||
| 261 | + | ||
| 262 | + @Test | ||
| 263 | + void update_targetSoftDeleted_throws40431() { | ||
| 264 | + UserEntity target = existingTargetUser(100); | ||
| 265 | + target.setBDeleted(true); | ||
| 266 | + when(userMapper.selectById(100)).thenReturn(target); | ||
| 267 | + | ||
| 268 | + assertThatThrownBy(() -> service.update(100, updateDto())) | ||
| 269 | + .isInstanceOf(BizException.class) | ||
| 270 | + .extracting(e -> ((BizException) e).getCode()) | ||
| 271 | + .isEqualTo(ErrorCode.USR_NOT_FOUND.getCode()); | ||
| 272 | + } | ||
| 273 | + | ||
| 274 | + @Test | ||
| 275 | + void update_staffNotFound_throws40421() { | ||
| 276 | + UserEntity target = existingTargetUser(101); | ||
| 277 | + when(userMapper.selectById(101)).thenReturn(target); | ||
| 278 | + UserUpdateDTO d = updateDto(); | ||
| 279 | + d.setIStaffId(999999); | ||
| 280 | + when(staffMapper.selectById(999999)).thenReturn(null); | ||
| 281 | + | ||
| 282 | + assertThatThrownBy(() -> service.update(101, d)) | ||
| 283 | + .isInstanceOf(BizException.class) | ||
| 284 | + .extracting(e -> ((BizException) e).getCode()) | ||
| 285 | + .isEqualTo(ErrorCode.STAFF_NOT_FOUND.getCode()); | ||
| 286 | + } | ||
| 287 | + | ||
| 288 | + @Test | ||
| 289 | + void update_staffSoftDeleted_throws40421() { | ||
| 290 | + UserEntity target = existingTargetUser(102); | ||
| 291 | + when(userMapper.selectById(102)).thenReturn(target); | ||
| 292 | + UserUpdateDTO d = updateDto(); | ||
| 293 | + d.setIStaffId(8); | ||
| 294 | + StaffEntity staff = new StaffEntity(); | ||
| 295 | + staff.setIIncrement(8); | ||
| 296 | + staff.setBDeleted(true); | ||
| 297 | + when(staffMapper.selectById(8)).thenReturn(staff); | ||
| 298 | + | ||
| 299 | + assertThatThrownBy(() -> service.update(102, d)) | ||
| 300 | + .isInstanceOf(BizException.class) | ||
| 301 | + .extracting(e -> ((BizException) e).getCode()) | ||
| 302 | + .isEqualTo(ErrorCode.STAFF_NOT_FOUND.getCode()); | ||
| 303 | + } | ||
| 304 | + | ||
| 305 | + @Test | ||
| 306 | + void update_permissionCategoryNotFound_throws40422() { | ||
| 307 | + UserEntity target = existingTargetUser(103); | ||
| 308 | + when(userMapper.selectById(103)).thenReturn(target); | ||
| 309 | + UserUpdateDTO d = updateDto(); | ||
| 310 | + d.setIStaffId(null); // 跳过 staff 校验 | ||
| 311 | + d.setPermissionCategoryIds(List.of(10, 999999)); | ||
| 312 | + when(permissionCategoryMapper.selectBatchIds(anyList())).thenReturn(List.of(cat(10))); | ||
| 313 | + | ||
| 314 | + assertThatThrownBy(() -> service.update(103, d)) | ||
| 315 | + .isInstanceOf(BizException.class) | ||
| 316 | + .extracting(e -> ((BizException) e).getCode()) | ||
| 317 | + .isEqualTo(ErrorCode.PERM_CATEGORY_NOT_FOUND.getCode()); | ||
| 318 | + } | ||
| 319 | + | ||
| 320 | + @Test | ||
| 321 | + void update_full_returnsVOWithUpdatedFields_andRebuildsPermissions() { | ||
| 322 | + UserEntity target = existingTargetUser(104); | ||
| 323 | + when(userMapper.selectById(104)).thenReturn(target); | ||
| 324 | + StaffEntity staff = new StaffEntity(); | ||
| 325 | + staff.setIIncrement(7); | ||
| 326 | + staff.setBDeleted(false); | ||
| 327 | + when(staffMapper.selectById(7)).thenReturn(staff); | ||
| 328 | + when(permissionCategoryMapper.selectBatchIds(anyList())).thenReturn(List.of(cat(10), cat(20))); | ||
| 329 | + | ||
| 330 | + UserVO vo = service.update(104, updateDto()); | ||
| 331 | + | ||
| 332 | + ArgumentCaptor<UserEntity> userCap = ArgumentCaptor.forClass(UserEntity.class); | ||
| 333 | + verify(userMapper).updateById(userCap.capture()); | ||
| 334 | + UserEntity saved = userCap.getValue(); | ||
| 335 | + // 已修改字段 | ||
| 336 | + assertThat(saved.getIStaffId()).isEqualTo(7); | ||
| 337 | + assertThat(saved.getSUserType()).isEqualTo("超级管理员"); | ||
| 338 | + assertThat(saved.getSLanguage()).isEqualTo("en"); | ||
| 339 | + assertThat(saved.getBCanModifyDocs()).isTrue(); | ||
| 340 | + // 保留字段 | ||
| 341 | + assertThat(saved.getSUserNo()).isEqualTo("u_orig"); | ||
| 342 | + assertThat(saved.getSUserName()).isEqualTo("alice_orig"); | ||
| 343 | + assertThat(saved.getSPasswordHash()).isEqualTo("$2a$10$origHash"); | ||
| 344 | + assertThat(saved.getSCreatedBy()).isEqualTo("system"); | ||
| 345 | + | ||
| 346 | + // 关联表先删后插 | ||
| 347 | + verify(userPermissionMapper).delete(any(Wrapper.class)); | ||
| 348 | + ArgumentCaptor<UserPermissionEntity> upCap = ArgumentCaptor.forClass(UserPermissionEntity.class); | ||
| 349 | + verify(userPermissionMapper, times(2)).insert(upCap.capture()); | ||
| 350 | + assertThat(upCap.getAllValues()).extracting(UserPermissionEntity::getICategoryId) | ||
| 351 | + .containsExactly(10, 20); | ||
| 352 | + assertThat(upCap.getAllValues()).extracting(UserPermissionEntity::getIUserId) | ||
| 353 | + .containsOnly(104); | ||
| 354 | + | ||
| 355 | + assertThat(vo.getPermissionCategoryIds()).containsExactly(10, 20); | ||
| 356 | + } | ||
| 357 | + | ||
| 358 | + @Test | ||
| 359 | + void update_partialNullBCanModifyDocs_keepsOriginal() { | ||
| 360 | + UserEntity target = existingTargetUser(105); | ||
| 361 | + target.setBCanModifyDocs(true); // 原值 true | ||
| 362 | + when(userMapper.selectById(105)).thenReturn(target); | ||
| 363 | + StaffEntity staff = new StaffEntity(); | ||
| 364 | + staff.setIIncrement(7); | ||
| 365 | + staff.setBDeleted(false); | ||
| 366 | + when(staffMapper.selectById(7)).thenReturn(staff); | ||
| 367 | + when(permissionCategoryMapper.selectBatchIds(anyList())).thenReturn(List.of(cat(10), cat(20))); | ||
| 368 | + | ||
| 369 | + UserUpdateDTO d = updateDto(); | ||
| 370 | + d.setBCanModifyDocs(null); // 期望保留原值 | ||
| 371 | + | ||
| 372 | + service.update(105, d); | ||
| 373 | + | ||
| 374 | + ArgumentCaptor<UserEntity> userCap = ArgumentCaptor.forClass(UserEntity.class); | ||
| 375 | + verify(userMapper).updateById(userCap.capture()); | ||
| 376 | + assertThat(userCap.getValue().getBCanModifyDocs()).isTrue(); | ||
| 377 | + } | ||
| 378 | + | ||
| 379 | + @Test | ||
| 380 | + void update_clearStaffId_setsToNull() { | ||
| 381 | + UserEntity target = existingTargetUser(106); | ||
| 382 | + target.setIStaffId(3); // 原本有 | ||
| 383 | + when(userMapper.selectById(106)).thenReturn(target); | ||
| 384 | + when(permissionCategoryMapper.selectBatchIds(anyList())).thenReturn(List.of(cat(10), cat(20))); | ||
| 385 | + | ||
| 386 | + UserUpdateDTO d = updateDto(); | ||
| 387 | + d.setIStaffId(null); // 显式清空 | ||
| 388 | + | ||
| 389 | + service.update(106, d); | ||
| 390 | + | ||
| 391 | + ArgumentCaptor<UserEntity> userCap = ArgumentCaptor.forClass(UserEntity.class); | ||
| 392 | + verify(userMapper).updateById(userCap.capture()); | ||
| 393 | + assertThat(userCap.getValue().getIStaffId()).isNull(); | ||
| 394 | + } | ||
| 395 | + | ||
| 396 | + @Test | ||
| 397 | + void update_emptyPermissionCategoryIds_clearsAllAssociations() { | ||
| 398 | + UserEntity target = existingTargetUser(107); | ||
| 399 | + when(userMapper.selectById(107)).thenReturn(target); | ||
| 400 | + StaffEntity staff = new StaffEntity(); | ||
| 401 | + staff.setIIncrement(7); | ||
| 402 | + staff.setBDeleted(false); | ||
| 403 | + when(staffMapper.selectById(7)).thenReturn(staff); | ||
| 404 | + | ||
| 405 | + UserUpdateDTO d = updateDto(); | ||
| 406 | + d.setPermissionCategoryIds(List.of()); // 清空全部 | ||
| 407 | + | ||
| 408 | + service.update(107, d); | ||
| 409 | + | ||
| 410 | + verify(userPermissionMapper).delete(any(Wrapper.class)); | ||
| 411 | + verify(userPermissionMapper, never()).insert((UserPermissionEntity) any()); | ||
| 412 | + } | ||
| 221 | } | 413 | } |