diff --git a/backend/src/main/java/com/xly/erp/module/usr/dto/CreateUserDTO.java b/backend/src/main/java/com/xly/erp/module/usr/dto/CreateUserDTO.java new file mode 100644 index 0000000..eb0c5fa --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/usr/dto/CreateUserDTO.java @@ -0,0 +1,52 @@ +package com.xly.erp.module.usr.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +import java.util.List; + +public class CreateUserDTO { + + @JsonProperty("sUserNo") + @NotBlank + @Size(max = 50) + private String sUserNo; + + @JsonProperty("sUserName") + @NotBlank + @Size(max = 50) + private String sUserName; + + @JsonProperty("iStaffId") + private Integer iStaffId; + + @JsonProperty("sUserType") + @NotBlank + private String sUserType; + + @JsonProperty("sLanguage") + @NotBlank + private String sLanguage; + + @JsonProperty("bCanModifyDocs") + private Boolean bCanModifyDocs; + + @JsonProperty("permissionCategoryIds") + private List permissionCategoryIds; + + public String getSUserNo() { return sUserNo; } + public void setSUserNo(String sUserNo) { this.sUserNo = sUserNo; } + public String getSUserName() { return sUserName; } + public void setSUserName(String sUserName) { this.sUserName = sUserName; } + public Integer getIStaffId() { return iStaffId; } + public void setIStaffId(Integer iStaffId) { this.iStaffId = iStaffId; } + public String getSUserType() { return sUserType; } + public void setSUserType(String sUserType) { this.sUserType = sUserType; } + public String getSLanguage() { return sLanguage; } + public void setSLanguage(String sLanguage) { this.sLanguage = sLanguage; } + public Boolean getBCanModifyDocs() { return bCanModifyDocs; } + public void setBCanModifyDocs(Boolean bCanModifyDocs) { this.bCanModifyDocs = bCanModifyDocs; } + public List getPermissionCategoryIds() { return permissionCategoryIds; } + public void setPermissionCategoryIds(List permissionCategoryIds) { this.permissionCategoryIds = permissionCategoryIds; } +} 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..ec396f7 --- /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.CreateUserDTO; + +import java.util.Map; + +public interface UserService { + Map create(CreateUserDTO 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..4a61f6d --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java @@ -0,0 +1,97 @@ +package com.xly.erp.module.usr.service.impl; + +import com.xly.erp.common.config.StubSecurityProperties; +import com.xly.erp.common.config.TenantProperties; +import com.xly.erp.common.exception.BizException; +import com.xly.erp.common.security.SecurityContextHelper; +import com.xly.erp.module.usr.dto.CreateUserDTO; +import com.xly.erp.module.usr.entity.User; +import com.xly.erp.module.usr.entity.UserPermission; +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 org.springframework.dao.DuplicateKeyException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Service +@Transactional(rollbackFor = Exception.class) +public class UserServiceImpl implements UserService { + + static final Set USER_TYPES = Set.of("普通用户", "超级管理员"); + static final Set LANGUAGES = Set.of("zh", "en", "zh-TW"); + static final String DEFAULT_PASSWORD = "666666"; + + private final UserMapper userMapper; + private final UserPermissionMapper userPermissionMapper; + private final StaffMapper staffMapper; + private final PermissionCategoryMapper permissionCategoryMapper; + private final TenantProperties tenant; + private final StubSecurityProperties stub; + private final BCryptPasswordEncoder passwordEncoder; + + public UserServiceImpl(UserMapper userMapper, + UserPermissionMapper userPermissionMapper, + StaffMapper staffMapper, + PermissionCategoryMapper permissionCategoryMapper, + TenantProperties tenant, + StubSecurityProperties stub, + BCryptPasswordEncoder passwordEncoder) { + this.userMapper = userMapper; + this.userPermissionMapper = userPermissionMapper; + this.staffMapper = staffMapper; + this.permissionCategoryMapper = permissionCategoryMapper; + this.tenant = tenant; + this.stub = stub; + this.passwordEncoder = passwordEncoder; + } + + @Override + public Map create(CreateUserDTO dto) { + User entity = new User(); + entity.setSBrandsId(tenant.getBrandsId()); + entity.setSSubsidiaryId(tenant.getSubsidiaryId()); + entity.setTCreateDate(LocalDateTime.now()); + entity.setSUserNo(dto.getSUserNo()); + entity.setSUserName(dto.getSUserName()); + entity.setIStaffId(dto.getIStaffId()); + entity.setSUserType(dto.getSUserType()); + entity.setSLanguage(dto.getSLanguage()); + entity.setBCanModifyDocs(dto.getBCanModifyDocs() != null ? dto.getBCanModifyDocs() : false); + entity.setSPasswordHash(passwordEncoder.encode(DEFAULT_PASSWORD)); + String authedUserNo = SecurityContextHelper.currentUserNo(); + String createdBy = authedUserNo != null ? authedUserNo : stub.getStubUserNo(); + entity.setSCreatedBy(createdBy); + entity.setBDeleted(false); + + userMapper.insert(entity); + + List ids = dto.getPermissionCategoryIds(); + if (ids != null && !ids.isEmpty()) { + for (Integer cid : ids) { + UserPermission rel = new UserPermission(); + rel.setSBrandsId(tenant.getBrandsId()); + rel.setSSubsidiaryId(tenant.getSubsidiaryId()); + rel.setTCreateDate(LocalDateTime.now()); + rel.setIUserId(entity.getIIncrement()); + rel.setICategoryId(cid); + rel.setSCreatedBy(createdBy); + userPermissionMapper.insert(rel); + } + } + + Map result = new LinkedHashMap<>(); + result.put("iIncrement", entity.getIIncrement()); + result.put("sUserNo", entity.getSUserNo()); + return result; + } +} 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..b89887b --- /dev/null +++ b/backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java @@ -0,0 +1,127 @@ +package com.xly.erp.module.usr.service; + +import com.xly.erp.common.config.StubSecurityProperties; +import com.xly.erp.common.config.TenantProperties; +import com.xly.erp.common.exception.BizException; +import com.xly.erp.module.usr.dto.CreateUserDTO; +import com.xly.erp.module.usr.entity.User; +import com.xly.erp.module.usr.entity.UserPermission; +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 org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +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.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class UserServiceImplTest { + + private UserMapper userMapper; + private UserPermissionMapper userPermissionMapper; + private StaffMapper staffMapper; + private PermissionCategoryMapper permissionCategoryMapper; + private BCryptPasswordEncoder encoder; + private UserServiceImpl service; + + @BeforeEach + void setUp() { + userMapper = mock(UserMapper.class); + userPermissionMapper = mock(UserPermissionMapper.class); + staffMapper = mock(StaffMapper.class); + permissionCategoryMapper = mock(PermissionCategoryMapper.class); + encoder = new BCryptPasswordEncoder(); + TenantProperties tenant = new TenantProperties(); + tenant.setBrandsId("XLY"); + tenant.setSubsidiaryId("XLY"); + StubSecurityProperties stub = new StubSecurityProperties(); + stub.setStubUserNo("STUB_ADMIN"); + + service = new UserServiceImpl(userMapper, userPermissionMapper, staffMapper, + permissionCategoryMapper, tenant, stub, encoder); + + lenient().when(userMapper.insert(any(User.class))).thenAnswer(inv -> { + User u = inv.getArgument(0); + u.setIIncrement(456); + return 1; + }); + lenient().when(userPermissionMapper.insert(any(UserPermission.class))).thenReturn(1); + } + + @AfterEach + void clearContext() { + SecurityContextHolder.clearContext(); + } + + @Test + void createWithValidDto_persistsUser_andUserPermissions() { + when(staffMapper.existsActiveById(7)).thenReturn(true); + when(permissionCategoryMapper.countActiveByIds(List.of(11, 22))).thenReturn(2); + + CreateUserDTO dto = baseDto(); + dto.setIStaffId(7); + dto.setPermissionCategoryIds(List.of(11, 22)); + + Map result = service.create(dto); + + assertThat(result).containsEntry("iIncrement", 456).containsEntry("sUserNo", "u001"); + ArgumentCaptor userCap = ArgumentCaptor.forClass(User.class); + verify(userMapper).insert(userCap.capture()); + User saved = userCap.getValue(); + assertThat(saved.getSBrandsId()).isEqualTo("XLY"); + assertThat(saved.getSCreatedBy()).isEqualTo("STUB_ADMIN"); + assertThat(saved.getTCreateDate()).isNotNull(); + assertThat(saved.getSPasswordHash()).startsWith("$2a$"); + assertThat(saved.getBDeleted()).isFalse(); + assertThat(saved.getIStaffId()).isEqualTo(7); + assertThat(saved.getBCanModifyDocs()).isFalse(); + + ArgumentCaptor permCap = ArgumentCaptor.forClass(UserPermission.class); + verify(userPermissionMapper, times(2)).insert(permCap.capture()); + List rels = permCap.getAllValues(); + assertThat(rels).extracting(UserPermission::getICategoryId).containsExactly(11, 22); + assertThat(rels).allMatch(r -> r.getIUserId().equals(456)); + assertThat(rels).allMatch(r -> r.getSBrandsId().equals("XLY")); + } + + @Test + void createWithoutPermissionCategoryIds_skipsUserPermissionInserts() { + CreateUserDTO dto = baseDto(); + dto.setPermissionCategoryIds(null); + + service.create(dto); + + verify(userMapper, times(1)).insert(any(User.class)); + verify(userPermissionMapper, never()).insert(any(UserPermission.class)); + } + + private CreateUserDTO baseDto() { + CreateUserDTO dto = new CreateUserDTO(); + dto.setSUserNo("u001"); + dto.setSUserName("用户1"); + dto.setSUserType("普通用户"); + dto.setSLanguage("zh"); + dto.setBCanModifyDocs(false); + return dto; + } +}