Commit 323b1ef4aeb2a23d56d8926ad9c8091bbdd8cc68

Authored by zichun
1 parent 8554c9ae

feat(usr): create user service REQ-USR-001

backend/src/main/java/com/xly/erp/module/usr/service/UserService.java 0 → 100644
  1 +package com.xly.erp.module.usr.service;
  2 +
  3 +import com.xly.erp.module.usr.dto.UserCreateDTO;
  4 +import com.xly.erp.module.usr.vo.UserVO;
  5 +
  6 +public interface UserService {
  7 + /** REQ-USR-001 用户新增 */
  8 + UserVO create(UserCreateDTO dto);
  9 +}
backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java 0 → 100644
  1 +package com.xly.erp.module.usr.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.xly.erp.common.exception.BizException;
  5 +import com.xly.erp.common.response.ErrorCode;
  6 +import com.xly.erp.module.usr.dto.UserCreateDTO;
  7 +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.UserEntity;
  10 +import com.xly.erp.module.usr.entity.UserPermissionEntity;
  11 +import com.xly.erp.module.usr.mapper.PermissionCategoryMapper;
  12 +import com.xly.erp.module.usr.mapper.StaffMapper;
  13 +import com.xly.erp.module.usr.mapper.UserMapper;
  14 +import com.xly.erp.module.usr.mapper.UserPermissionMapper;
  15 +import com.xly.erp.module.usr.service.UserService;
  16 +import com.xly.erp.module.usr.vo.UserVO;
  17 +import lombok.RequiredArgsConstructor;
  18 +import org.springframework.dao.DuplicateKeyException;
  19 +import org.springframework.security.crypto.password.PasswordEncoder;
  20 +import org.springframework.stereotype.Service;
  21 +import org.springframework.transaction.annotation.Transactional;
  22 +
  23 +import java.time.LocalDateTime;
  24 +import java.util.ArrayList;
  25 +import java.util.List;
  26 +
  27 +/** REQ-USR-001 用户新增 */
  28 +@Service
  29 +@RequiredArgsConstructor
  30 +public class UserServiceImpl implements UserService {
  31 +
  32 + private static final String INITIAL_PASSWORD = "666666";
  33 +
  34 + private final UserMapper userMapper;
  35 + private final StaffMapper staffMapper;
  36 + private final PermissionCategoryMapper permissionCategoryMapper;
  37 + private final UserPermissionMapper userPermissionMapper;
  38 + private final PasswordEncoder passwordEncoder;
  39 +
  40 + @Override
  41 + @Transactional(rollbackFor = Exception.class)
  42 + public UserVO create(UserCreateDTO dto) {
  43 + // 1. 唯一性预检:sUserName / sUserNo(bDeleted=0 范围)
  44 + Long existsByName = userMapper.selectCount(
  45 + new LambdaQueryWrapper<UserEntity>()
  46 + .eq(UserEntity::getSUserName, dto.getSUserName())
  47 + .eq(UserEntity::getBDeleted, false));
  48 + if (existsByName != null && existsByName > 0L) {
  49 + throw new BizException(ErrorCode.USR_USER_NAME_OR_NO_DUP);
  50 + }
  51 + Long existsByNo = userMapper.selectCount(
  52 + new LambdaQueryWrapper<UserEntity>()
  53 + .eq(UserEntity::getSUserNo, dto.getSUserNo())
  54 + .eq(UserEntity::getBDeleted, false));
  55 + if (existsByNo != null && existsByNo > 0L) {
  56 + throw new BizException(ErrorCode.USR_USER_NAME_OR_NO_DUP);
  57 + }
  58 +
  59 + // 2. iStaffId 校验
  60 + if (dto.getIStaffId() != null) {
  61 + StaffEntity staff = staffMapper.selectById(dto.getIStaffId());
  62 + if (staff == null || Boolean.TRUE.equals(staff.getBDeleted())) {
  63 + throw new BizException(ErrorCode.STAFF_NOT_FOUND);
  64 + }
  65 + }
  66 +
  67 + // 3. 权限分类校验:批量查;要求每个 id 都存在且未软删除
  68 + List<Integer> categoryIds = dto.getPermissionCategoryIds() == null
  69 + ? new ArrayList<>() : dto.getPermissionCategoryIds();
  70 + if (!categoryIds.isEmpty()) {
  71 + List<PermissionCategoryEntity> found = permissionCategoryMapper.selectBatchIds(categoryIds);
  72 + if (found.size() != categoryIds.size()
  73 + || found.stream().anyMatch(p -> Boolean.TRUE.equals(p.getBDeleted()))) {
  74 + throw new BizException(ErrorCode.PERM_CATEGORY_NOT_FOUND);
  75 + }
  76 + }
  77 +
  78 + // 4. 构造 UserEntity 并 insert
  79 + UserEntity user = new UserEntity();
  80 + user.setSUserNo(dto.getSUserNo());
  81 + user.setSUserName(dto.getSUserName());
  82 + user.setIStaffId(dto.getIStaffId());
  83 + user.setSUserType(dto.getSUserType());
  84 + user.setSLanguage(dto.getSLanguage());
  85 + user.setBCanModifyDocs(dto.getBCanModifyDocs() != null ? dto.getBCanModifyDocs() : Boolean.FALSE);
  86 + user.setSPasswordHash(passwordEncoder.encode(INITIAL_PASSWORD));
  87 + user.setTCreateDate(LocalDateTime.now());
  88 + user.setBDeleted(Boolean.FALSE);
  89 + // tLastLoginDate / sCreatedBy / sBrandsId / sSubsidiaryId / sId / tDeletedDate / sDeletedBy 留 null
  90 +
  91 + try {
  92 + userMapper.insert(user);
  93 + } catch (DuplicateKeyException dup) {
  94 + throw new BizException(ErrorCode.USR_USER_NAME_OR_NO_DUP);
  95 + }
  96 +
  97 + // 5. 批量 insert UserPermission
  98 + for (Integer categoryId : categoryIds) {
  99 + UserPermissionEntity up = new UserPermissionEntity();
  100 + up.setIUserId(user.getIIncrement());
  101 + up.setICategoryId(categoryId);
  102 + up.setTCreateDate(LocalDateTime.now());
  103 + // sCreatedBy 留 null(REQ-USR-004 后回填)
  104 + userPermissionMapper.insert(up);
  105 + }
  106 +
  107 + return UserVO.from(user, categoryIds);
  108 + }
  109 +}
backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java 0 → 100644
  1 +package com.xly.erp.module.usr.service;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.Wrapper;
  4 +import com.xly.erp.common.exception.BizException;
  5 +import com.xly.erp.common.response.ErrorCode;
  6 +import com.xly.erp.module.usr.dto.UserCreateDTO;
  7 +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.UserEntity;
  10 +import com.xly.erp.module.usr.entity.UserPermissionEntity;
  11 +import com.xly.erp.module.usr.mapper.PermissionCategoryMapper;
  12 +import com.xly.erp.module.usr.mapper.StaffMapper;
  13 +import com.xly.erp.module.usr.mapper.UserMapper;
  14 +import com.xly.erp.module.usr.mapper.UserPermissionMapper;
  15 +import com.xly.erp.module.usr.service.impl.UserServiceImpl;
  16 +import com.xly.erp.module.usr.vo.UserVO;
  17 +import org.junit.jupiter.api.Test;
  18 +import org.junit.jupiter.api.extension.ExtendWith;
  19 +import org.mockito.ArgumentCaptor;
  20 +import org.mockito.InjectMocks;
  21 +import org.mockito.Mock;
  22 +import org.mockito.junit.jupiter.MockitoExtension;
  23 +import org.springframework.dao.DuplicateKeyException;
  24 +import org.springframework.security.crypto.password.PasswordEncoder;
  25 +
  26 +import java.util.List;
  27 +
  28 +import static org.assertj.core.api.Assertions.assertThat;
  29 +import static org.assertj.core.api.Assertions.assertThatThrownBy;
  30 +import static org.mockito.ArgumentMatchers.any;
  31 +import static org.mockito.ArgumentMatchers.anyList;
  32 +import static org.mockito.Mockito.never;
  33 +import static org.mockito.Mockito.times;
  34 +import static org.mockito.Mockito.verify;
  35 +import static org.mockito.Mockito.when;
  36 +
  37 +@ExtendWith(MockitoExtension.class)
  38 +class UserServiceImplTest {
  39 +
  40 + @Mock UserMapper userMapper;
  41 + @Mock StaffMapper staffMapper;
  42 + @Mock PermissionCategoryMapper permissionCategoryMapper;
  43 + @Mock UserPermissionMapper userPermissionMapper;
  44 + @Mock PasswordEncoder passwordEncoder;
  45 +
  46 + @InjectMocks UserServiceImpl service;
  47 +
  48 + private UserCreateDTO baseDto() {
  49 + UserCreateDTO d = new UserCreateDTO();
  50 + d.setSUserNo("u001");
  51 + d.setSUserName("alice");
  52 + d.setSUserType("普通用户");
  53 + d.setSLanguage("zh");
  54 + return d;
  55 + }
  56 +
  57 + @Test
  58 + void create_minimalFields_returnsVOWithBCryptHash() {
  59 + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L);
  60 + when(passwordEncoder.encode("666666")).thenReturn("$2a$10$mockhash");
  61 + when(userMapper.insert((UserEntity) any())).thenAnswer(inv -> {
  62 + UserEntity u = inv.getArgument(0);
  63 + u.setIIncrement(101);
  64 + return 1;
  65 + });
  66 +
  67 + UserVO vo = service.create(baseDto());
  68 +
  69 + assertThat(vo.getIIncrement()).isEqualTo(101);
  70 + assertThat(vo.getSUserName()).isEqualTo("alice");
  71 + assertThat(vo.getBCanModifyDocs()).isFalse();
  72 + assertThat(vo.getPermissionCategoryIds()).isEmpty();
  73 +
  74 + ArgumentCaptor<UserEntity> cap = ArgumentCaptor.forClass(UserEntity.class);
  75 + verify(userMapper).insert(cap.capture());
  76 + UserEntity saved = cap.getValue();
  77 + assertThat(saved.getSPasswordHash()).isEqualTo("$2a$10$mockhash");
  78 + assertThat(saved.getBDeleted()).isFalse();
  79 + assertThat(saved.getTCreateDate()).isNotNull();
  80 + assertThat(saved.getSCreatedBy()).isNull();
  81 + assertThat(saved.getSBrandsId()).isNull();
  82 + }
  83 +
  84 + @Test
  85 + void create_withStaffAndPermissions_writesAssociation() {
  86 + UserCreateDTO d = baseDto();
  87 + d.setIStaffId(7);
  88 + d.setPermissionCategoryIds(List.of(1, 2, 3));
  89 +
  90 + StaffEntity staff = new StaffEntity();
  91 + staff.setIIncrement(7);
  92 + staff.setBDeleted(false);
  93 + when(staffMapper.selectById(7)).thenReturn(staff);
  94 +
  95 + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L);
  96 + when(permissionCategoryMapper.selectBatchIds(anyList())).thenReturn(List.of(
  97 + cat(1), cat(2), cat(3)
  98 + ));
  99 + when(passwordEncoder.encode("666666")).thenReturn("$2a$10$h");
  100 + when(userMapper.insert((UserEntity) any())).thenAnswer(inv -> {
  101 + UserEntity u = inv.getArgument(0);
  102 + u.setIIncrement(202);
  103 + return 1;
  104 + });
  105 +
  106 + UserVO vo = service.create(d);
  107 +
  108 + assertThat(vo.getIIncrement()).isEqualTo(202);
  109 + assertThat(vo.getIStaffId()).isEqualTo(7);
  110 + assertThat(vo.getPermissionCategoryIds()).containsExactly(1, 2, 3);
  111 +
  112 + ArgumentCaptor<UserPermissionEntity> upCap = ArgumentCaptor.forClass(UserPermissionEntity.class);
  113 + verify(userPermissionMapper, times(3)).insert(upCap.capture());
  114 + List<UserPermissionEntity> ups = upCap.getAllValues();
  115 + assertThat(ups).extracting(UserPermissionEntity::getIUserId).containsOnly(202);
  116 + assertThat(ups).extracting(UserPermissionEntity::getICategoryId).containsExactly(1, 2, 3);
  117 + }
  118 +
  119 + @Test
  120 + void create_duplicateUserName_throws40921() {
  121 + // 第一次 selectCount(sUserName) 返回 1
  122 + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(1L);
  123 +
  124 + assertThatThrownBy(() -> service.create(baseDto()))
  125 + .isInstanceOf(BizException.class)
  126 + .extracting(e -> ((BizException) e).getCode())
  127 + .isEqualTo(ErrorCode.USR_USER_NAME_OR_NO_DUP.getCode());
  128 + verify(userMapper, never()).insert((UserEntity) any());
  129 + }
  130 +
  131 + @Test
  132 + void create_duplicateUserNo_throws40921() {
  133 + // 第一次 (sUserName) 返回 0;第二次 (sUserNo) 返回 1
  134 + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L, 1L);
  135 +
  136 + assertThatThrownBy(() -> service.create(baseDto()))
  137 + .isInstanceOf(BizException.class)
  138 + .extracting(e -> ((BizException) e).getCode())
  139 + .isEqualTo(ErrorCode.USR_USER_NAME_OR_NO_DUP.getCode());
  140 + verify(userMapper, never()).insert((UserEntity) any());
  141 + }
  142 +
  143 + @Test
  144 + void create_staffNotFound_throws40421() {
  145 + UserCreateDTO d = baseDto();
  146 + d.setIStaffId(999999);
  147 + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L);
  148 + when(staffMapper.selectById(999999)).thenReturn(null);
  149 +
  150 + assertThatThrownBy(() -> service.create(d))
  151 + .isInstanceOf(BizException.class)
  152 + .extracting(e -> ((BizException) e).getCode())
  153 + .isEqualTo(ErrorCode.STAFF_NOT_FOUND.getCode());
  154 + }
  155 +
  156 + @Test
  157 + void create_staffSoftDeleted_throws40421() {
  158 + UserCreateDTO d = baseDto();
  159 + d.setIStaffId(5);
  160 + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L);
  161 + StaffEntity deleted = new StaffEntity();
  162 + deleted.setIIncrement(5);
  163 + deleted.setBDeleted(true);
  164 + when(staffMapper.selectById(5)).thenReturn(deleted);
  165 +
  166 + assertThatThrownBy(() -> service.create(d))
  167 + .isInstanceOf(BizException.class)
  168 + .extracting(e -> ((BizException) e).getCode())
  169 + .isEqualTo(ErrorCode.STAFF_NOT_FOUND.getCode());
  170 + }
  171 +
  172 + @Test
  173 + void create_permissionCategoryNotFound_throws40422() {
  174 + UserCreateDTO d = baseDto();
  175 + d.setPermissionCategoryIds(List.of(1, 999999));
  176 + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L);
  177 + when(permissionCategoryMapper.selectBatchIds(anyList())).thenReturn(List.of(cat(1))); // 只返回 1 条,缺 999999
  178 +
  179 + assertThatThrownBy(() -> service.create(d))
  180 + .isInstanceOf(BizException.class)
  181 + .extracting(e -> ((BizException) e).getCode())
  182 + .isEqualTo(ErrorCode.PERM_CATEGORY_NOT_FOUND.getCode());
  183 + verify(userMapper, never()).insert((UserEntity) any());
  184 + }
  185 +
  186 + @Test
  187 + void create_emptyPermissionCategoryIds_doesNotInsertAssociation() {
  188 + UserCreateDTO d = baseDto();
  189 + d.setPermissionCategoryIds(List.of());
  190 + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L);
  191 + when(passwordEncoder.encode("666666")).thenReturn("$2a$10$h");
  192 + when(userMapper.insert((UserEntity) any())).thenAnswer(inv -> {
  193 + ((UserEntity) inv.getArgument(0)).setIIncrement(303);
  194 + return 1;
  195 + });
  196 +
  197 + service.create(d);
  198 +
  199 + verify(userPermissionMapper, never()).insert((UserPermissionEntity) any());
  200 + }
  201 +
  202 + @Test
  203 + void create_concurrentDuplicate_dupKeyException_mappedTo40921() {
  204 + when(userMapper.selectCount(any(Wrapper.class))).thenReturn(0L);
  205 + when(passwordEncoder.encode("666666")).thenReturn("$2a$10$h");
  206 + when(userMapper.insert((UserEntity) any()))
  207 + .thenThrow(new DuplicateKeyException("uk_user_name"));
  208 +
  209 + assertThatThrownBy(() -> service.create(baseDto()))
  210 + .isInstanceOf(BizException.class)
  211 + .extracting(e -> ((BizException) e).getCode())
  212 + .isEqualTo(ErrorCode.USR_USER_NAME_OR_NO_DUP.getCode());
  213 + }
  214 +
  215 + private static PermissionCategoryEntity cat(int id) {
  216 + PermissionCategoryEntity p = new PermissionCategoryEntity();
  217 + p.setIIncrement(id);
  218 + p.setBDeleted(false);
  219 + return p;
  220 + }
  221 +}