From 323b1ef4aeb2a23d56d8926ad9c8091bbdd8cc68 Mon Sep 17 00:00:00 2001 From: zichun Date: Wed, 6 May 2026 21:15:26 +0800 Subject: [PATCH] feat(usr): create user service REQ-USR-001 --- backend/src/main/java/com/xly/erp/module/usr/service/UserService.java | 9 +++++++++ backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java | 221 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 339 insertions(+), 0 deletions(-) create mode 100644 backend/src/main/java/com/xly/erp/module/usr/service/UserService.java create mode 100644 backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java create mode 100644 backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java 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 new file mode 100644 index 0000000..e3030d9 --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/usr/service/UserService.java @@ -0,0 +1,9 @@ +package com.xly.erp.module.usr.service; + +import com.xly.erp.module.usr.dto.UserCreateDTO; +import com.xly.erp.module.usr.vo.UserVO; + +public interface UserService { + /** REQ-USR-001 用户新增 */ + UserVO create(UserCreateDTO 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 new file mode 100644 index 0000000..03f039b --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java @@ -0,0 +1,109 @@ +package com.xly.erp.module.usr.service.impl; + +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.entity.PermissionCategoryEntity; +import com.xly.erp.module.usr.entity.StaffEntity; +import com.xly.erp.module.usr.entity.UserEntity; +import com.xly.erp.module.usr.entity.UserPermissionEntity; +import com.xly.erp.module.usr.mapper.PermissionCategoryMapper; +import com.xly.erp.module.usr.mapper.StaffMapper; +import com.xly.erp.module.usr.mapper.UserMapper; +import com.xly.erp.module.usr.mapper.UserPermissionMapper; +import com.xly.erp.module.usr.service.UserService; +import com.xly.erp.module.usr.vo.UserVO; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** REQ-USR-001 用户新增 */ +@Service +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + + private static final String INITIAL_PASSWORD = "666666"; + + private final UserMapper userMapper; + private final StaffMapper staffMapper; + private final PermissionCategoryMapper permissionCategoryMapper; + private final UserPermissionMapper userPermissionMapper; + private final PasswordEncoder passwordEncoder; + + @Override + @Transactional(rollbackFor = Exception.class) + public UserVO create(UserCreateDTO dto) { + // 1. 唯一性预检:sUserName / sUserNo(bDeleted=0 范围) + Long existsByName = userMapper.selectCount( + new LambdaQueryWrapper() + .eq(UserEntity::getSUserName, dto.getSUserName()) + .eq(UserEntity::getBDeleted, false)); + if (existsByName != null && existsByName > 0L) { + throw new BizException(ErrorCode.USR_USER_NAME_OR_NO_DUP); + } + Long existsByNo = userMapper.selectCount( + new LambdaQueryWrapper() + .eq(UserEntity::getSUserNo, dto.getSUserNo()) + .eq(UserEntity::getBDeleted, false)); + if (existsByNo != null && existsByNo > 0L) { + throw new BizException(ErrorCode.USR_USER_NAME_OR_NO_DUP); + } + + // 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. 权限分类校验:批量查;要求每个 id 都存在且未软删除 + 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. 构造 UserEntity 并 insert + UserEntity user = new UserEntity(); + user.setSUserNo(dto.getSUserNo()); + user.setSUserName(dto.getSUserName()); + user.setIStaffId(dto.getIStaffId()); + user.setSUserType(dto.getSUserType()); + user.setSLanguage(dto.getSLanguage()); + user.setBCanModifyDocs(dto.getBCanModifyDocs() != null ? dto.getBCanModifyDocs() : Boolean.FALSE); + user.setSPasswordHash(passwordEncoder.encode(INITIAL_PASSWORD)); + user.setTCreateDate(LocalDateTime.now()); + user.setBDeleted(Boolean.FALSE); + // tLastLoginDate / sCreatedBy / sBrandsId / sSubsidiaryId / sId / tDeletedDate / sDeletedBy 留 null + + try { + userMapper.insert(user); + } catch (DuplicateKeyException dup) { + throw new BizException(ErrorCode.USR_USER_NAME_OR_NO_DUP); + } + + // 5. 批量 insert UserPermission + for (Integer categoryId : categoryIds) { + UserPermissionEntity up = new UserPermissionEntity(); + up.setIUserId(user.getIIncrement()); + up.setICategoryId(categoryId); + up.setTCreateDate(LocalDateTime.now()); + // sCreatedBy 留 null(REQ-USR-004 后回填) + userPermissionMapper.insert(up); + } + + return UserVO.from(user, 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 new file mode 100644 index 0000000..99787b5 --- /dev/null +++ b/backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java @@ -0,0 +1,221 @@ +package com.xly.erp.module.usr.service; + +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.entity.PermissionCategoryEntity; +import com.xly.erp.module.usr.entity.StaffEntity; +import com.xly.erp.module.usr.entity.UserEntity; +import com.xly.erp.module.usr.entity.UserPermissionEntity; +import com.xly.erp.module.usr.mapper.PermissionCategoryMapper; +import com.xly.erp.module.usr.mapper.StaffMapper; +import com.xly.erp.module.usr.mapper.UserMapper; +import com.xly.erp.module.usr.mapper.UserPermissionMapper; +import com.xly.erp.module.usr.service.impl.UserServiceImpl; +import com.xly.erp.module.usr.vo.UserVO; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class UserServiceImplTest { + + @Mock UserMapper userMapper; + @Mock StaffMapper staffMapper; + @Mock PermissionCategoryMapper permissionCategoryMapper; + @Mock UserPermissionMapper userPermissionMapper; + @Mock PasswordEncoder passwordEncoder; + + @InjectMocks UserServiceImpl service; + + private UserCreateDTO baseDto() { + UserCreateDTO d = new UserCreateDTO(); + d.setSUserNo("u001"); + d.setSUserName("alice"); + d.setSUserType("普通用户"); + d.setSLanguage("zh"); + return d; + } + + @Test + void create_minimalFields_returnsVOWithBCryptHash() { + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L); + when(passwordEncoder.encode("666666")).thenReturn("$2a$10$mockhash"); + when(userMapper.insert((UserEntity) any())).thenAnswer(inv -> { + UserEntity u = inv.getArgument(0); + u.setIIncrement(101); + return 1; + }); + + UserVO vo = service.create(baseDto()); + + assertThat(vo.getIIncrement()).isEqualTo(101); + assertThat(vo.getSUserName()).isEqualTo("alice"); + assertThat(vo.getBCanModifyDocs()).isFalse(); + assertThat(vo.getPermissionCategoryIds()).isEmpty(); + + ArgumentCaptor cap = ArgumentCaptor.forClass(UserEntity.class); + verify(userMapper).insert(cap.capture()); + UserEntity saved = cap.getValue(); + assertThat(saved.getSPasswordHash()).isEqualTo("$2a$10$mockhash"); + assertThat(saved.getBDeleted()).isFalse(); + assertThat(saved.getTCreateDate()).isNotNull(); + assertThat(saved.getSCreatedBy()).isNull(); + assertThat(saved.getSBrandsId()).isNull(); + } + + @Test + void create_withStaffAndPermissions_writesAssociation() { + UserCreateDTO d = baseDto(); + d.setIStaffId(7); + d.setPermissionCategoryIds(List.of(1, 2, 3)); + + StaffEntity staff = new StaffEntity(); + staff.setIIncrement(7); + staff.setBDeleted(false); + when(staffMapper.selectById(7)).thenReturn(staff); + + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L); + when(permissionCategoryMapper.selectBatchIds(anyList())).thenReturn(List.of( + cat(1), cat(2), cat(3) + )); + when(passwordEncoder.encode("666666")).thenReturn("$2a$10$h"); + when(userMapper.insert((UserEntity) any())).thenAnswer(inv -> { + UserEntity u = inv.getArgument(0); + u.setIIncrement(202); + return 1; + }); + + UserVO vo = service.create(d); + + assertThat(vo.getIIncrement()).isEqualTo(202); + assertThat(vo.getIStaffId()).isEqualTo(7); + assertThat(vo.getPermissionCategoryIds()).containsExactly(1, 2, 3); + + ArgumentCaptor upCap = ArgumentCaptor.forClass(UserPermissionEntity.class); + verify(userPermissionMapper, times(3)).insert(upCap.capture()); + List ups = upCap.getAllValues(); + assertThat(ups).extracting(UserPermissionEntity::getIUserId).containsOnly(202); + assertThat(ups).extracting(UserPermissionEntity::getICategoryId).containsExactly(1, 2, 3); + } + + @Test + void create_duplicateUserName_throws40921() { + // 第一次 selectCount(sUserName) 返回 1 + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(1L); + + assertThatThrownBy(() -> service.create(baseDto())) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.USR_USER_NAME_OR_NO_DUP.getCode()); + verify(userMapper, never()).insert((UserEntity) any()); + } + + @Test + void create_duplicateUserNo_throws40921() { + // 第一次 (sUserName) 返回 0;第二次 (sUserNo) 返回 1 + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L, 1L); + + assertThatThrownBy(() -> service.create(baseDto())) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.USR_USER_NAME_OR_NO_DUP.getCode()); + verify(userMapper, never()).insert((UserEntity) any()); + } + + @Test + void create_staffNotFound_throws40421() { + UserCreateDTO d = baseDto(); + d.setIStaffId(999999); + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L); + when(staffMapper.selectById(999999)).thenReturn(null); + + assertThatThrownBy(() -> service.create(d)) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.STAFF_NOT_FOUND.getCode()); + } + + @Test + void create_staffSoftDeleted_throws40421() { + UserCreateDTO d = baseDto(); + d.setIStaffId(5); + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L); + StaffEntity deleted = new StaffEntity(); + deleted.setIIncrement(5); + deleted.setBDeleted(true); + when(staffMapper.selectById(5)).thenReturn(deleted); + + assertThatThrownBy(() -> service.create(d)) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.STAFF_NOT_FOUND.getCode()); + } + + @Test + void create_permissionCategoryNotFound_throws40422() { + UserCreateDTO d = baseDto(); + d.setPermissionCategoryIds(List.of(1, 999999)); + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L); + when(permissionCategoryMapper.selectBatchIds(anyList())).thenReturn(List.of(cat(1))); // 只返回 1 条,缺 999999 + + assertThatThrownBy(() -> service.create(d)) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.PERM_CATEGORY_NOT_FOUND.getCode()); + verify(userMapper, never()).insert((UserEntity) any()); + } + + @Test + void create_emptyPermissionCategoryIds_doesNotInsertAssociation() { + UserCreateDTO d = baseDto(); + d.setPermissionCategoryIds(List.of()); + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L); + when(passwordEncoder.encode("666666")).thenReturn("$2a$10$h"); + when(userMapper.insert((UserEntity) any())).thenAnswer(inv -> { + ((UserEntity) inv.getArgument(0)).setIIncrement(303); + return 1; + }); + + service.create(d); + + verify(userPermissionMapper, never()).insert((UserPermissionEntity) any()); + } + + @Test + void create_concurrentDuplicate_dupKeyException_mappedTo40921() { + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L); + when(passwordEncoder.encode("666666")).thenReturn("$2a$10$h"); + when(userMapper.insert((UserEntity) any())) + .thenThrow(new DuplicateKeyException("uk_user_name")); + + assertThatThrownBy(() -> service.create(baseDto())) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.USR_USER_NAME_OR_NO_DUP.getCode()); + } + + private static PermissionCategoryEntity cat(int id) { + PermissionCategoryEntity p = new PermissionCategoryEntity(); + p.setIIncrement(id); + p.setBDeleted(false); + return p; + } +} -- libgit2 0.22.2