Commit 71b613d6d9a4de4f5387587d9957a543dc8af085

Authored by zichun
1 parent ea270697

feat(usr): UserService.createUser 主插入 + 隐式字段 + BCrypt 哈希 REQ-USR-001

backend/src/main/java/com/xly/test4/module/usr/service/UserService.java 0 → 100644
  1 +package com.xly.test4.module.usr.service;
  2 +
  3 +import com.xly.test4.module.usr.dto.UserCreateDTO;
  4 +import com.xly.test4.module.usr.vo.UserCreateVO;
  5 +
  6 +public interface UserService {
  7 +
  8 + UserCreateVO createUser(UserCreateDTO dto);
  9 +}
backend/src/main/java/com/xly/test4/module/usr/service/impl/UserServiceImpl.java 0 → 100644
  1 +package com.xly.test4.module.usr.service.impl;
  2 +
  3 +import com.xly.test4.common.security.CurrentUser;
  4 +import com.xly.test4.common.security.CurrentUserContext;
  5 +import com.xly.test4.module.usr.converter.UserConverter;
  6 +import com.xly.test4.module.usr.dto.UserCreateDTO;
  7 +import com.xly.test4.module.usr.entity.User;
  8 +import com.xly.test4.module.usr.mapper.UserMapper;
  9 +import com.xly.test4.module.usr.service.UserService;
  10 +import com.xly.test4.module.usr.vo.UserCreateVO;
  11 +import org.springframework.beans.factory.annotation.Value;
  12 +import org.springframework.security.crypto.password.PasswordEncoder;
  13 +import org.springframework.stereotype.Service;
  14 +import org.springframework.transaction.annotation.Transactional;
  15 +
  16 +@Service
  17 +public class UserServiceImpl implements UserService {
  18 +
  19 + private final UserMapper userMapper;
  20 + private final UserConverter userConverter;
  21 + private final PasswordEncoder passwordEncoder;
  22 + private final String defaultPassword;
  23 +
  24 + public UserServiceImpl(UserMapper userMapper,
  25 + UserConverter userConverter,
  26 + PasswordEncoder passwordEncoder,
  27 + @Value("${app.security.default-password}") String defaultPassword) {
  28 + this.userMapper = userMapper;
  29 + this.userConverter = userConverter;
  30 + this.passwordEncoder = passwordEncoder;
  31 + this.defaultPassword = defaultPassword;
  32 + }
  33 +
  34 + @Override
  35 + @Transactional(rollbackFor = Exception.class)
  36 + public UserCreateVO createUser(UserCreateDTO dto) {
  37 + CurrentUser current = CurrentUserContext.current();
  38 +
  39 + User user = userConverter.toEntity(dto);
  40 + user.setSBrandsId(current.getBrandsId());
  41 + user.setSSubsidiaryId(current.getSubsidiaryId());
  42 + user.setSCreatedBy(current.getUserName());
  43 + user.setIIsDisabled(0);
  44 + user.setILoginFailCount(0);
  45 +
  46 + String rawPassword = dto.getPassword() != null ? dto.getPassword() : defaultPassword;
  47 + user.setSPasswordHash(passwordEncoder.encode(rawPassword));
  48 +
  49 + userMapper.insert(user);
  50 +
  51 + return userConverter.toVO(user);
  52 + }
  53 +}
backend/src/test/java/com/xly/test4/module/usr/service/impl/UserServiceImplTest.java 0 → 100644
  1 +package com.xly.test4.module.usr.service.impl;
  2 +
  3 +import com.xly.test4.common.security.CurrentUser;
  4 +import com.xly.test4.common.security.CurrentUserContext;
  5 +import com.xly.test4.module.usr.converter.UserConverter;
  6 +import com.xly.test4.module.usr.dto.UserCreateDTO;
  7 +import com.xly.test4.module.usr.entity.User;
  8 +import com.xly.test4.module.usr.mapper.UserMapper;
  9 +import com.xly.test4.module.usr.vo.UserCreateVO;
  10 +import org.junit.jupiter.api.AfterEach;
  11 +import org.junit.jupiter.api.BeforeEach;
  12 +import org.junit.jupiter.api.Test;
  13 +import org.mockito.ArgumentCaptor;
  14 +import org.mockito.MockedStatic;
  15 +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  16 +import org.springframework.security.crypto.password.PasswordEncoder;
  17 +
  18 +import java.util.List;
  19 +
  20 +import static org.assertj.core.api.Assertions.assertThat;
  21 +import static org.mockito.ArgumentMatchers.any;
  22 +import static org.mockito.Mockito.mock;
  23 +import static org.mockito.Mockito.mockStatic;
  24 +import static org.mockito.Mockito.when;
  25 +
  26 +class UserServiceImplTest {
  27 +
  28 + private UserMapper userMapper;
  29 + private UserConverter userConverter;
  30 + private PasswordEncoder passwordEncoder;
  31 + private UserServiceImpl service;
  32 + private MockedStatic<CurrentUserContext> ctxMock;
  33 +
  34 + private static final CurrentUser ADMIN_CONTEXT = CurrentUser.builder()
  35 + .userId(1)
  36 + .userName("admin")
  37 + .brandsId("BR-DEFAULT")
  38 + .subsidiaryId("SUB-DEFAULT")
  39 + .authorities(List.of("usr:user:create"))
  40 + .build();
  41 +
  42 + @BeforeEach
  43 + void setUp() {
  44 + userMapper = mock(UserMapper.class);
  45 + userConverter = mock(UserConverter.class);
  46 + passwordEncoder = new BCryptPasswordEncoder();
  47 + service = new UserServiceImpl(userMapper, userConverter, passwordEncoder, "666666");
  48 + ctxMock = mockStatic(CurrentUserContext.class);
  49 + ctxMock.when(CurrentUserContext::current).thenReturn(ADMIN_CONTEXT);
  50 + }
  51 +
  52 + @AfterEach
  53 + void tearDown() {
  54 + ctxMock.close();
  55 + }
  56 +
  57 + private UserCreateDTO baseDTO() {
  58 + UserCreateDTO dto = new UserCreateDTO();
  59 + dto.setUserCode("U-NEW-001");
  60 + dto.setUserName("newuser");
  61 + dto.setUserType("NORMAL");
  62 + dto.setLanguage("zh-CN");
  63 + dto.setCanEditDoc(false);
  64 + return dto;
  65 + }
  66 +
  67 + private void stubConverterReturnsEmptyUser() {
  68 + when(userConverter.toEntity(any(UserCreateDTO.class))).thenAnswer(inv -> new User());
  69 + when(userConverter.toVO(any(User.class))).thenAnswer(inv -> {
  70 + User u = inv.getArgument(0);
  71 + return UserCreateVO.builder().userId(u.getIIncrement()).userCode(u.getSUserCode()).build();
  72 + });
  73 + when(userMapper.insert(any(User.class))).thenAnswer(inv -> {
  74 + User u = inv.getArgument(0);
  75 + u.setIIncrement(42);
  76 + return 1;
  77 + });
  78 + }
  79 +
  80 + @Test
  81 + void createUser_minimalDTO_writesSecurityContextFieldsAndHashesPassword() {
  82 + stubConverterReturnsEmptyUser();
  83 + UserCreateDTO dto = baseDTO();
  84 + dto.setPassword("MyPass123");
  85 +
  86 + UserCreateVO vo = service.createUser(dto);
  87 +
  88 + ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
  89 + org.mockito.Mockito.verify(userMapper).insert(captor.capture());
  90 + User written = captor.getValue();
  91 +
  92 + assertThat(written.getSCreatedBy()).isEqualTo("admin");
  93 + assertThat(written.getSBrandsId()).isEqualTo("BR-DEFAULT");
  94 + assertThat(written.getSSubsidiaryId()).isEqualTo("SUB-DEFAULT");
  95 + assertThat(written.getIIsDisabled()).isZero();
  96 + assertThat(written.getILoginFailCount()).isZero();
  97 + assertThat(written.getSPasswordHash()).startsWith("$2");
  98 + assertThat(written.getSPasswordHash()).hasSize(60);
  99 + assertThat(passwordEncoder.matches("MyPass123", written.getSPasswordHash())).isTrue();
  100 +
  101 + assertThat(vo.getUserId()).isEqualTo(42);
  102 + }
  103 +
  104 + @Test
  105 + void createUser_noPasswordInDTO_usesDefaultPassword666666() {
  106 + stubConverterReturnsEmptyUser();
  107 + UserCreateDTO dto = baseDTO();
  108 + dto.setPassword(null);
  109 +
  110 + service.createUser(dto);
  111 +
  112 + ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
  113 + org.mockito.Mockito.verify(userMapper).insert(captor.capture());
  114 + assertThat(passwordEncoder.matches("666666", captor.getValue().getSPasswordHash())).isTrue();
  115 + }
  116 +}