Commit e27b394bfff1a96a1ef4f2cf5183c5de7bbd13ae
1 parent
4cf3a3bb
feat(usr): user create dto + service happy path REQ-USR-001
Showing
4 changed files
with
285 additions
and
0 deletions
backend/src/main/java/com/xly/erp/module/usr/dto/CreateUserDTO.java
0 → 100644
| 1 | +package com.xly.erp.module.usr.dto; | ||
| 2 | + | ||
| 3 | +import com.fasterxml.jackson.annotation.JsonProperty; | ||
| 4 | +import jakarta.validation.constraints.NotBlank; | ||
| 5 | +import jakarta.validation.constraints.Size; | ||
| 6 | + | ||
| 7 | +import java.util.List; | ||
| 8 | + | ||
| 9 | +public class CreateUserDTO { | ||
| 10 | + | ||
| 11 | + @JsonProperty("sUserNo") | ||
| 12 | + @NotBlank | ||
| 13 | + @Size(max = 50) | ||
| 14 | + private String sUserNo; | ||
| 15 | + | ||
| 16 | + @JsonProperty("sUserName") | ||
| 17 | + @NotBlank | ||
| 18 | + @Size(max = 50) | ||
| 19 | + private String sUserName; | ||
| 20 | + | ||
| 21 | + @JsonProperty("iStaffId") | ||
| 22 | + private Integer iStaffId; | ||
| 23 | + | ||
| 24 | + @JsonProperty("sUserType") | ||
| 25 | + @NotBlank | ||
| 26 | + private String sUserType; | ||
| 27 | + | ||
| 28 | + @JsonProperty("sLanguage") | ||
| 29 | + @NotBlank | ||
| 30 | + private String sLanguage; | ||
| 31 | + | ||
| 32 | + @JsonProperty("bCanModifyDocs") | ||
| 33 | + private Boolean bCanModifyDocs; | ||
| 34 | + | ||
| 35 | + @JsonProperty("permissionCategoryIds") | ||
| 36 | + private List<Integer> permissionCategoryIds; | ||
| 37 | + | ||
| 38 | + public String getSUserNo() { return sUserNo; } | ||
| 39 | + public void setSUserNo(String sUserNo) { this.sUserNo = sUserNo; } | ||
| 40 | + public String getSUserName() { return sUserName; } | ||
| 41 | + public void setSUserName(String sUserName) { this.sUserName = sUserName; } | ||
| 42 | + public Integer getIStaffId() { return iStaffId; } | ||
| 43 | + public void setIStaffId(Integer iStaffId) { this.iStaffId = iStaffId; } | ||
| 44 | + public String getSUserType() { return sUserType; } | ||
| 45 | + public void setSUserType(String sUserType) { this.sUserType = sUserType; } | ||
| 46 | + public String getSLanguage() { return sLanguage; } | ||
| 47 | + public void setSLanguage(String sLanguage) { this.sLanguage = sLanguage; } | ||
| 48 | + public Boolean getBCanModifyDocs() { return bCanModifyDocs; } | ||
| 49 | + public void setBCanModifyDocs(Boolean bCanModifyDocs) { this.bCanModifyDocs = bCanModifyDocs; } | ||
| 50 | + public List<Integer> getPermissionCategoryIds() { return permissionCategoryIds; } | ||
| 51 | + public void setPermissionCategoryIds(List<Integer> permissionCategoryIds) { this.permissionCategoryIds = permissionCategoryIds; } | ||
| 52 | +} |
backend/src/main/java/com/xly/erp/module/usr/service/UserService.java
0 → 100644
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.xly.erp.common.config.StubSecurityProperties; | ||
| 4 | +import com.xly.erp.common.config.TenantProperties; | ||
| 5 | +import com.xly.erp.common.exception.BizException; | ||
| 6 | +import com.xly.erp.common.security.SecurityContextHelper; | ||
| 7 | +import com.xly.erp.module.usr.dto.CreateUserDTO; | ||
| 8 | +import com.xly.erp.module.usr.entity.User; | ||
| 9 | +import com.xly.erp.module.usr.entity.UserPermission; | ||
| 10 | +import com.xly.erp.module.usr.mapper.PermissionCategoryMapper; | ||
| 11 | +import com.xly.erp.module.usr.mapper.StaffMapper; | ||
| 12 | +import com.xly.erp.module.usr.mapper.UserMapper; | ||
| 13 | +import com.xly.erp.module.usr.mapper.UserPermissionMapper; | ||
| 14 | +import com.xly.erp.module.usr.service.UserService; | ||
| 15 | +import org.springframework.dao.DuplicateKeyException; | ||
| 16 | +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | ||
| 17 | +import org.springframework.stereotype.Service; | ||
| 18 | +import org.springframework.transaction.annotation.Transactional; | ||
| 19 | + | ||
| 20 | +import java.time.LocalDateTime; | ||
| 21 | +import java.util.LinkedHashMap; | ||
| 22 | +import java.util.List; | ||
| 23 | +import java.util.Map; | ||
| 24 | +import java.util.Set; | ||
| 25 | + | ||
| 26 | +@Service | ||
| 27 | +@Transactional(rollbackFor = Exception.class) | ||
| 28 | +public class UserServiceImpl implements UserService { | ||
| 29 | + | ||
| 30 | + static final Set<String> USER_TYPES = Set.of("普通用户", "超级管理员"); | ||
| 31 | + static final Set<String> LANGUAGES = Set.of("zh", "en", "zh-TW"); | ||
| 32 | + static final String DEFAULT_PASSWORD = "666666"; | ||
| 33 | + | ||
| 34 | + private final UserMapper userMapper; | ||
| 35 | + private final UserPermissionMapper userPermissionMapper; | ||
| 36 | + private final StaffMapper staffMapper; | ||
| 37 | + private final PermissionCategoryMapper permissionCategoryMapper; | ||
| 38 | + private final TenantProperties tenant; | ||
| 39 | + private final StubSecurityProperties stub; | ||
| 40 | + private final BCryptPasswordEncoder passwordEncoder; | ||
| 41 | + | ||
| 42 | + public UserServiceImpl(UserMapper userMapper, | ||
| 43 | + UserPermissionMapper userPermissionMapper, | ||
| 44 | + StaffMapper staffMapper, | ||
| 45 | + PermissionCategoryMapper permissionCategoryMapper, | ||
| 46 | + TenantProperties tenant, | ||
| 47 | + StubSecurityProperties stub, | ||
| 48 | + BCryptPasswordEncoder passwordEncoder) { | ||
| 49 | + this.userMapper = userMapper; | ||
| 50 | + this.userPermissionMapper = userPermissionMapper; | ||
| 51 | + this.staffMapper = staffMapper; | ||
| 52 | + this.permissionCategoryMapper = permissionCategoryMapper; | ||
| 53 | + this.tenant = tenant; | ||
| 54 | + this.stub = stub; | ||
| 55 | + this.passwordEncoder = passwordEncoder; | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + @Override | ||
| 59 | + public Map<String, Object> create(CreateUserDTO dto) { | ||
| 60 | + User entity = new User(); | ||
| 61 | + entity.setSBrandsId(tenant.getBrandsId()); | ||
| 62 | + entity.setSSubsidiaryId(tenant.getSubsidiaryId()); | ||
| 63 | + entity.setTCreateDate(LocalDateTime.now()); | ||
| 64 | + entity.setSUserNo(dto.getSUserNo()); | ||
| 65 | + entity.setSUserName(dto.getSUserName()); | ||
| 66 | + entity.setIStaffId(dto.getIStaffId()); | ||
| 67 | + entity.setSUserType(dto.getSUserType()); | ||
| 68 | + entity.setSLanguage(dto.getSLanguage()); | ||
| 69 | + entity.setBCanModifyDocs(dto.getBCanModifyDocs() != null ? dto.getBCanModifyDocs() : false); | ||
| 70 | + entity.setSPasswordHash(passwordEncoder.encode(DEFAULT_PASSWORD)); | ||
| 71 | + String authedUserNo = SecurityContextHelper.currentUserNo(); | ||
| 72 | + String createdBy = authedUserNo != null ? authedUserNo : stub.getStubUserNo(); | ||
| 73 | + entity.setSCreatedBy(createdBy); | ||
| 74 | + entity.setBDeleted(false); | ||
| 75 | + | ||
| 76 | + userMapper.insert(entity); | ||
| 77 | + | ||
| 78 | + List<Integer> ids = dto.getPermissionCategoryIds(); | ||
| 79 | + if (ids != null && !ids.isEmpty()) { | ||
| 80 | + for (Integer cid : ids) { | ||
| 81 | + UserPermission rel = new UserPermission(); | ||
| 82 | + rel.setSBrandsId(tenant.getBrandsId()); | ||
| 83 | + rel.setSSubsidiaryId(tenant.getSubsidiaryId()); | ||
| 84 | + rel.setTCreateDate(LocalDateTime.now()); | ||
| 85 | + rel.setIUserId(entity.getIIncrement()); | ||
| 86 | + rel.setICategoryId(cid); | ||
| 87 | + rel.setSCreatedBy(createdBy); | ||
| 88 | + userPermissionMapper.insert(rel); | ||
| 89 | + } | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + Map<String, Object> result = new LinkedHashMap<>(); | ||
| 93 | + result.put("iIncrement", entity.getIIncrement()); | ||
| 94 | + result.put("sUserNo", entity.getSUserNo()); | ||
| 95 | + return result; | ||
| 96 | + } | ||
| 97 | +} |
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.xly.erp.common.config.StubSecurityProperties; | ||
| 4 | +import com.xly.erp.common.config.TenantProperties; | ||
| 5 | +import com.xly.erp.common.exception.BizException; | ||
| 6 | +import com.xly.erp.module.usr.dto.CreateUserDTO; | ||
| 7 | +import com.xly.erp.module.usr.entity.User; | ||
| 8 | +import com.xly.erp.module.usr.entity.UserPermission; | ||
| 9 | +import com.xly.erp.module.usr.mapper.PermissionCategoryMapper; | ||
| 10 | +import com.xly.erp.module.usr.mapper.StaffMapper; | ||
| 11 | +import com.xly.erp.module.usr.mapper.UserMapper; | ||
| 12 | +import com.xly.erp.module.usr.mapper.UserPermissionMapper; | ||
| 13 | +import com.xly.erp.module.usr.service.impl.UserServiceImpl; | ||
| 14 | +import org.junit.jupiter.api.AfterEach; | ||
| 15 | +import org.junit.jupiter.api.BeforeEach; | ||
| 16 | +import org.junit.jupiter.api.Test; | ||
| 17 | +import org.mockito.ArgumentCaptor; | ||
| 18 | +import org.springframework.dao.DuplicateKeyException; | ||
| 19 | +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
| 20 | +import org.springframework.security.core.context.SecurityContextHolder; | ||
| 21 | +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | ||
| 22 | + | ||
| 23 | +import java.util.Collections; | ||
| 24 | +import java.util.List; | ||
| 25 | +import java.util.Map; | ||
| 26 | + | ||
| 27 | +import static org.assertj.core.api.Assertions.assertThat; | ||
| 28 | +import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
| 29 | +import static org.mockito.ArgumentMatchers.any; | ||
| 30 | +import static org.mockito.ArgumentMatchers.anyList; | ||
| 31 | +import static org.mockito.Mockito.lenient; | ||
| 32 | +import static org.mockito.Mockito.mock; | ||
| 33 | +import static org.mockito.Mockito.never; | ||
| 34 | +import static org.mockito.Mockito.times; | ||
| 35 | +import static org.mockito.Mockito.verify; | ||
| 36 | +import static org.mockito.Mockito.when; | ||
| 37 | + | ||
| 38 | +class UserServiceImplTest { | ||
| 39 | + | ||
| 40 | + private UserMapper userMapper; | ||
| 41 | + private UserPermissionMapper userPermissionMapper; | ||
| 42 | + private StaffMapper staffMapper; | ||
| 43 | + private PermissionCategoryMapper permissionCategoryMapper; | ||
| 44 | + private BCryptPasswordEncoder encoder; | ||
| 45 | + private UserServiceImpl service; | ||
| 46 | + | ||
| 47 | + @BeforeEach | ||
| 48 | + void setUp() { | ||
| 49 | + userMapper = mock(UserMapper.class); | ||
| 50 | + userPermissionMapper = mock(UserPermissionMapper.class); | ||
| 51 | + staffMapper = mock(StaffMapper.class); | ||
| 52 | + permissionCategoryMapper = mock(PermissionCategoryMapper.class); | ||
| 53 | + encoder = new BCryptPasswordEncoder(); | ||
| 54 | + TenantProperties tenant = new TenantProperties(); | ||
| 55 | + tenant.setBrandsId("XLY"); | ||
| 56 | + tenant.setSubsidiaryId("XLY"); | ||
| 57 | + StubSecurityProperties stub = new StubSecurityProperties(); | ||
| 58 | + stub.setStubUserNo("STUB_ADMIN"); | ||
| 59 | + | ||
| 60 | + service = new UserServiceImpl(userMapper, userPermissionMapper, staffMapper, | ||
| 61 | + permissionCategoryMapper, tenant, stub, encoder); | ||
| 62 | + | ||
| 63 | + lenient().when(userMapper.insert(any(User.class))).thenAnswer(inv -> { | ||
| 64 | + User u = inv.getArgument(0); | ||
| 65 | + u.setIIncrement(456); | ||
| 66 | + return 1; | ||
| 67 | + }); | ||
| 68 | + lenient().when(userPermissionMapper.insert(any(UserPermission.class))).thenReturn(1); | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + @AfterEach | ||
| 72 | + void clearContext() { | ||
| 73 | + SecurityContextHolder.clearContext(); | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + @Test | ||
| 77 | + void createWithValidDto_persistsUser_andUserPermissions() { | ||
| 78 | + when(staffMapper.existsActiveById(7)).thenReturn(true); | ||
| 79 | + when(permissionCategoryMapper.countActiveByIds(List.of(11, 22))).thenReturn(2); | ||
| 80 | + | ||
| 81 | + CreateUserDTO dto = baseDto(); | ||
| 82 | + dto.setIStaffId(7); | ||
| 83 | + dto.setPermissionCategoryIds(List.of(11, 22)); | ||
| 84 | + | ||
| 85 | + Map<String, Object> result = service.create(dto); | ||
| 86 | + | ||
| 87 | + assertThat(result).containsEntry("iIncrement", 456).containsEntry("sUserNo", "u001"); | ||
| 88 | + ArgumentCaptor<User> userCap = ArgumentCaptor.forClass(User.class); | ||
| 89 | + verify(userMapper).insert(userCap.capture()); | ||
| 90 | + User saved = userCap.getValue(); | ||
| 91 | + assertThat(saved.getSBrandsId()).isEqualTo("XLY"); | ||
| 92 | + assertThat(saved.getSCreatedBy()).isEqualTo("STUB_ADMIN"); | ||
| 93 | + assertThat(saved.getTCreateDate()).isNotNull(); | ||
| 94 | + assertThat(saved.getSPasswordHash()).startsWith("$2a$"); | ||
| 95 | + assertThat(saved.getBDeleted()).isFalse(); | ||
| 96 | + assertThat(saved.getIStaffId()).isEqualTo(7); | ||
| 97 | + assertThat(saved.getBCanModifyDocs()).isFalse(); | ||
| 98 | + | ||
| 99 | + ArgumentCaptor<UserPermission> permCap = ArgumentCaptor.forClass(UserPermission.class); | ||
| 100 | + verify(userPermissionMapper, times(2)).insert(permCap.capture()); | ||
| 101 | + List<UserPermission> rels = permCap.getAllValues(); | ||
| 102 | + assertThat(rels).extracting(UserPermission::getICategoryId).containsExactly(11, 22); | ||
| 103 | + assertThat(rels).allMatch(r -> r.getIUserId().equals(456)); | ||
| 104 | + assertThat(rels).allMatch(r -> r.getSBrandsId().equals("XLY")); | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + @Test | ||
| 108 | + void createWithoutPermissionCategoryIds_skipsUserPermissionInserts() { | ||
| 109 | + CreateUserDTO dto = baseDto(); | ||
| 110 | + dto.setPermissionCategoryIds(null); | ||
| 111 | + | ||
| 112 | + service.create(dto); | ||
| 113 | + | ||
| 114 | + verify(userMapper, times(1)).insert(any(User.class)); | ||
| 115 | + verify(userPermissionMapper, never()).insert(any(UserPermission.class)); | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + private CreateUserDTO baseDto() { | ||
| 119 | + CreateUserDTO dto = new CreateUserDTO(); | ||
| 120 | + dto.setSUserNo("u001"); | ||
| 121 | + dto.setSUserName("用户1"); | ||
| 122 | + dto.setSUserType("普通用户"); | ||
| 123 | + dto.setSLanguage("zh"); | ||
| 124 | + dto.setBCanModifyDocs(false); | ||
| 125 | + return dto; | ||
| 126 | + } | ||
| 127 | +} |