From 3f1b9e89007a308b451ea4c6d30db4e9f8a78dda Mon Sep 17 00:00:00 2001 From: zichun Date: Wed, 6 May 2026 21:36:49 +0800 Subject: [PATCH] feat(usr): update user service REQ-USR-002 --- backend/src/main/java/com/xly/erp/module/usr/service/UserService.java | 4 ++++ backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java | 192 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 252 insertions(+), 0 deletions(-) diff --git a/backend/src/main/java/com/xly/erp/module/usr/service/UserService.java b/backend/src/main/java/com/xly/erp/module/usr/service/UserService.java index e3030d9..de9c23e 100644 --- a/backend/src/main/java/com/xly/erp/module/usr/service/UserService.java +++ b/backend/src/main/java/com/xly/erp/module/usr/service/UserService.java @@ -1,9 +1,13 @@ package com.xly.erp.module.usr.service; import com.xly.erp.module.usr.dto.UserCreateDTO; +import com.xly.erp.module.usr.dto.UserUpdateDTO; import com.xly.erp.module.usr.vo.UserVO; public interface UserService { /** REQ-USR-001 用户新增 */ UserVO create(UserCreateDTO dto); + + /** REQ-USR-002 用户修改 */ + UserVO update(Integer id, UserUpdateDTO dto); } diff --git a/backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java b/backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java index 03f039b..ace1fbf 100644 --- a/backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java +++ b/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; import com.xly.erp.common.exception.BizException; import com.xly.erp.common.response.ErrorCode; import com.xly.erp.module.usr.dto.UserCreateDTO; +import com.xly.erp.module.usr.dto.UserUpdateDTO; import com.xly.erp.module.usr.entity.PermissionCategoryEntity; import com.xly.erp.module.usr.entity.StaffEntity; import com.xly.erp.module.usr.entity.UserEntity; @@ -106,4 +107,59 @@ public class UserServiceImpl implements UserService { return UserVO.from(user, categoryIds); } + + /** REQ-USR-002 用户修改 */ + @Override + @Transactional(rollbackFor = Exception.class) + public UserVO update(Integer id, UserUpdateDTO dto) { + // 1. 目标用户存在 + 未软删除 + UserEntity target = userMapper.selectById(id); + if (target == null || Boolean.TRUE.equals(target.getBDeleted())) { + throw new BizException(ErrorCode.USR_NOT_FOUND); + } + + // 2. iStaffId 校验(仅当非空) + if (dto.getIStaffId() != null) { + StaffEntity staff = staffMapper.selectById(dto.getIStaffId()); + if (staff == null || Boolean.TRUE.equals(staff.getBDeleted())) { + throw new BizException(ErrorCode.STAFF_NOT_FOUND); + } + } + + // 3. 权限分类校验(仅当非空) + List categoryIds = dto.getPermissionCategoryIds() == null + ? new ArrayList<>() : dto.getPermissionCategoryIds(); + if (!categoryIds.isEmpty()) { + List found = permissionCategoryMapper.selectBatchIds(categoryIds); + if (found.size() != categoryIds.size() + || found.stream().anyMatch(p -> Boolean.TRUE.equals(p.getBDeleted()))) { + throw new BizException(ErrorCode.PERM_CATEGORY_NOT_FOUND); + } + } + + // 4. 字段合并到 target(保留 sUserNo / sUserName / sPasswordHash 等保护字段) + target.setIStaffId(dto.getIStaffId()); // 含 null 清空(依赖 iStaffId.IGNORED 策略) + target.setSUserType(dto.getSUserType()); + target.setSLanguage(dto.getSLanguage()); + if (dto.getBCanModifyDocs() != null) { + target.setBCanModifyDocs(dto.getBCanModifyDocs()); + } + + // 5. 落库 user + userMapper.updateById(target); + + // 6. 重建权限关联:先删后插(清空原有,再按 dto 插入) + userPermissionMapper.delete( + new LambdaQueryWrapper() + .eq(UserPermissionEntity::getIUserId, id)); + for (Integer categoryId : categoryIds) { + UserPermissionEntity up = new UserPermissionEntity(); + up.setIUserId(id); + up.setICategoryId(categoryId); + up.setTCreateDate(LocalDateTime.now()); + userPermissionMapper.insert(up); + } + + return UserVO.from(target, categoryIds); + } } diff --git a/backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java b/backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java index 99787b5..5bc9a73 100644 --- a/backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java +++ b/backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.xly.erp.common.exception.BizException; import com.xly.erp.common.response.ErrorCode; import com.xly.erp.module.usr.dto.UserCreateDTO; +import com.xly.erp.module.usr.dto.UserUpdateDTO; import com.xly.erp.module.usr.entity.PermissionCategoryEntity; import com.xly.erp.module.usr.entity.StaffEntity; import com.xly.erp.module.usr.entity.UserEntity; @@ -218,4 +219,195 @@ class UserServiceImplTest { p.setBDeleted(false); return p; } + + // ============================================================ + // REQ-USR-002 update 系列 + // ============================================================ + + private UserUpdateDTO updateDto() { + UserUpdateDTO d = new UserUpdateDTO(); + d.setIStaffId(7); + d.setSUserType("超级管理员"); + d.setSLanguage("en"); + d.setBCanModifyDocs(true); + d.setPermissionCategoryIds(List.of(10, 20)); + return d; + } + + private UserEntity existingTargetUser(int id) { + UserEntity u = new UserEntity(); + u.setIIncrement(id); + u.setSUserNo("u_orig"); + u.setSUserName("alice_orig"); + u.setIStaffId(3); + u.setSUserType("普通用户"); + u.setSLanguage("zh"); + u.setBCanModifyDocs(false); + u.setSPasswordHash("$2a$10$origHash"); + u.setBDeleted(false); + u.setSCreatedBy("system"); + return u; + } + + @Test + void update_targetNotFound_throws40431() { + when(userMapper.selectById(99)).thenReturn(null); + + assertThatThrownBy(() -> service.update(99, updateDto())) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.USR_NOT_FOUND.getCode()); + } + + @Test + void update_targetSoftDeleted_throws40431() { + UserEntity target = existingTargetUser(100); + target.setBDeleted(true); + when(userMapper.selectById(100)).thenReturn(target); + + assertThatThrownBy(() -> service.update(100, updateDto())) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.USR_NOT_FOUND.getCode()); + } + + @Test + void update_staffNotFound_throws40421() { + UserEntity target = existingTargetUser(101); + when(userMapper.selectById(101)).thenReturn(target); + UserUpdateDTO d = updateDto(); + d.setIStaffId(999999); + when(staffMapper.selectById(999999)).thenReturn(null); + + assertThatThrownBy(() -> service.update(101, d)) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.STAFF_NOT_FOUND.getCode()); + } + + @Test + void update_staffSoftDeleted_throws40421() { + UserEntity target = existingTargetUser(102); + when(userMapper.selectById(102)).thenReturn(target); + UserUpdateDTO d = updateDto(); + d.setIStaffId(8); + StaffEntity staff = new StaffEntity(); + staff.setIIncrement(8); + staff.setBDeleted(true); + when(staffMapper.selectById(8)).thenReturn(staff); + + assertThatThrownBy(() -> service.update(102, d)) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.STAFF_NOT_FOUND.getCode()); + } + + @Test + void update_permissionCategoryNotFound_throws40422() { + UserEntity target = existingTargetUser(103); + when(userMapper.selectById(103)).thenReturn(target); + UserUpdateDTO d = updateDto(); + d.setIStaffId(null); // 跳过 staff 校验 + d.setPermissionCategoryIds(List.of(10, 999999)); + when(permissionCategoryMapper.selectBatchIds(anyList())).thenReturn(List.of(cat(10))); + + assertThatThrownBy(() -> service.update(103, d)) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.PERM_CATEGORY_NOT_FOUND.getCode()); + } + + @Test + void update_full_returnsVOWithUpdatedFields_andRebuildsPermissions() { + UserEntity target = existingTargetUser(104); + when(userMapper.selectById(104)).thenReturn(target); + StaffEntity staff = new StaffEntity(); + staff.setIIncrement(7); + staff.setBDeleted(false); + when(staffMapper.selectById(7)).thenReturn(staff); + when(permissionCategoryMapper.selectBatchIds(anyList())).thenReturn(List.of(cat(10), cat(20))); + + UserVO vo = service.update(104, updateDto()); + + ArgumentCaptor userCap = ArgumentCaptor.forClass(UserEntity.class); + verify(userMapper).updateById(userCap.capture()); + UserEntity saved = userCap.getValue(); + // 已修改字段 + assertThat(saved.getIStaffId()).isEqualTo(7); + assertThat(saved.getSUserType()).isEqualTo("超级管理员"); + assertThat(saved.getSLanguage()).isEqualTo("en"); + assertThat(saved.getBCanModifyDocs()).isTrue(); + // 保留字段 + assertThat(saved.getSUserNo()).isEqualTo("u_orig"); + assertThat(saved.getSUserName()).isEqualTo("alice_orig"); + assertThat(saved.getSPasswordHash()).isEqualTo("$2a$10$origHash"); + assertThat(saved.getSCreatedBy()).isEqualTo("system"); + + // 关联表先删后插 + verify(userPermissionMapper).delete(any(Wrapper.class)); + ArgumentCaptor upCap = ArgumentCaptor.forClass(UserPermissionEntity.class); + verify(userPermissionMapper, times(2)).insert(upCap.capture()); + assertThat(upCap.getAllValues()).extracting(UserPermissionEntity::getICategoryId) + .containsExactly(10, 20); + assertThat(upCap.getAllValues()).extracting(UserPermissionEntity::getIUserId) + .containsOnly(104); + + assertThat(vo.getPermissionCategoryIds()).containsExactly(10, 20); + } + + @Test + void update_partialNullBCanModifyDocs_keepsOriginal() { + UserEntity target = existingTargetUser(105); + target.setBCanModifyDocs(true); // 原值 true + when(userMapper.selectById(105)).thenReturn(target); + StaffEntity staff = new StaffEntity(); + staff.setIIncrement(7); + staff.setBDeleted(false); + when(staffMapper.selectById(7)).thenReturn(staff); + when(permissionCategoryMapper.selectBatchIds(anyList())).thenReturn(List.of(cat(10), cat(20))); + + UserUpdateDTO d = updateDto(); + d.setBCanModifyDocs(null); // 期望保留原值 + + service.update(105, d); + + ArgumentCaptor userCap = ArgumentCaptor.forClass(UserEntity.class); + verify(userMapper).updateById(userCap.capture()); + assertThat(userCap.getValue().getBCanModifyDocs()).isTrue(); + } + + @Test + void update_clearStaffId_setsToNull() { + UserEntity target = existingTargetUser(106); + target.setIStaffId(3); // 原本有 + when(userMapper.selectById(106)).thenReturn(target); + when(permissionCategoryMapper.selectBatchIds(anyList())).thenReturn(List.of(cat(10), cat(20))); + + UserUpdateDTO d = updateDto(); + d.setIStaffId(null); // 显式清空 + + service.update(106, d); + + ArgumentCaptor userCap = ArgumentCaptor.forClass(UserEntity.class); + verify(userMapper).updateById(userCap.capture()); + assertThat(userCap.getValue().getIStaffId()).isNull(); + } + + @Test + void update_emptyPermissionCategoryIds_clearsAllAssociations() { + UserEntity target = existingTargetUser(107); + when(userMapper.selectById(107)).thenReturn(target); + StaffEntity staff = new StaffEntity(); + staff.setIIncrement(7); + staff.setBDeleted(false); + when(staffMapper.selectById(7)).thenReturn(staff); + + UserUpdateDTO d = updateDto(); + d.setPermissionCategoryIds(List.of()); // 清空全部 + + service.update(107, d); + + verify(userPermissionMapper).delete(any(Wrapper.class)); + verify(userPermissionMapper, never()).insert((UserPermissionEntity) any()); + } } -- libgit2 0.22.2