diff --git a/backend/src/main/java/com/xly/erp/module/usr/service/UserCreateService.java b/backend/src/main/java/com/xly/erp/module/usr/service/UserCreateService.java new file mode 100644 index 0000000..9c55cdf --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/usr/service/UserCreateService.java @@ -0,0 +1,17 @@ +package com.xly.erp.module.usr.service; + +import com.xly.erp.module.usr.dto.CreateUserReq; +import com.xly.erp.module.usr.vo.CreateUserVo; + +public interface UserCreateService { + /** + * 新建用户 + 权限分类授权。 + * REQ-USR-002。 + * + * @param req 已通过 jakarta 校验的请求体 + * @param operatorUsername 当前登录用户(写入 sCreatedBy / sGrantedBy) + * @throws com.xly.erp.common.exception.BizException + * 40004 employee / permissionCategory 不存在 / 40901 用户名重复 / 40902 用户号重复 + */ + CreateUserVo create(CreateUserReq req, String operatorUsername); +} diff --git a/backend/src/main/java/com/xly/erp/module/usr/service/impl/UserCreateServiceImpl.java b/backend/src/main/java/com/xly/erp/module/usr/service/impl/UserCreateServiceImpl.java new file mode 100644 index 0000000..69b3f4a --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/usr/service/impl/UserCreateServiceImpl.java @@ -0,0 +1,112 @@ +package com.xly.erp.module.usr.service.impl; + +import com.xly.erp.common.exception.BizException; +import com.xly.erp.common.response.ErrorCode; +import com.xly.erp.module.usr.dto.CreateUserReq; +import com.xly.erp.module.usr.entity.SysEmployee; +import com.xly.erp.module.usr.entity.SysUser; +import com.xly.erp.module.usr.entity.SysUserPermissionCategory; +import com.xly.erp.module.usr.mapper.SysEmployeeMapper; +import com.xly.erp.module.usr.mapper.SysPermissionCategoryMapper; +import com.xly.erp.module.usr.mapper.SysUserMapper; +import com.xly.erp.module.usr.mapper.SysUserPermissionCategoryMapper; +import com.xly.erp.module.usr.service.UserCreateService; +import com.xly.erp.module.usr.vo.CreateUserVo; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class UserCreateServiceImpl implements UserCreateService { + + static final String INITIAL_PASSWORD = "666666"; + + private final SysUserMapper userMapper; + private final SysEmployeeMapper employeeMapper; + private final SysPermissionCategoryMapper permissionCategoryMapper; + private final SysUserPermissionCategoryMapper userPermissionCategoryMapper; + private final BCryptPasswordEncoder passwordEncoder; + + @Override + @Transactional + public CreateUserVo create(CreateUserReq req, String operatorUsername) { + // 1. 唯一性预检(返友好错误码;DB 唯一索引兜底并发场景) + if (userMapper.existsByUsername(req.getUsername())) { + throw new BizException(ErrorCode.CONFLICT_USERNAME, "用户名已存在"); + } + if (userMapper.existsByUserCode(req.getUserCode())) { + throw new BizException(ErrorCode.CONFLICT_USERCODE, "用户号已存在"); + } + + // 2. employee 外键校验 + if (req.getEmployeeId() != null) { + SysEmployee emp = employeeMapper.selectById(req.getEmployeeId()); + if (emp == null || Integer.valueOf(1).equals(emp.getIIsDeleted())) { + throw new BizException(ErrorCode.COMPANY_NOT_FOUND, "指定的员工不存在或已删除"); + } + } + + // 3. permissionCategory 外键校验(批量) + List pcIds = req.getPermissionCategoryIds(); + if (pcIds != null && !pcIds.isEmpty()) { + int active = permissionCategoryMapper.countActiveByIds(pcIds); + if (active != pcIds.size()) { + throw new BizException(ErrorCode.COMPANY_NOT_FOUND, + "指定的权限分类含不存在或已删除项"); + } + } + + // 4. 写入 sys_user + SysUser user = new SysUser(); + user.setSUsername(req.getUsername()); + user.setSUserCode(req.getUserCode()); + user.setSPasswordHash(passwordEncoder.encode(INITIAL_PASSWORD)); + user.setIEmployeeId(req.getEmployeeId()); + user.setSUserType(req.getUserType()); + user.setSLanguage(req.getLanguage()); + user.setICanEditDocument(Boolean.TRUE.equals(req.getCanEditDocument()) ? 1 : 0); + user.setIIsDeleted(0); + user.setIFailedLoginCount(0); + user.setSCreatedBy(operatorUsername); + try { + userMapper.insert(user); + } catch (DataIntegrityViolationException e) { + String msg = e.getMessage() == null ? "" : e.getMessage(); + if (msg.contains("uk_sys_user_username")) { + throw new BizException(ErrorCode.CONFLICT_USERNAME, "用户名已存在"); + } + if (msg.contains("uk_sys_user_code")) { + throw new BizException(ErrorCode.CONFLICT_USERCODE, "用户号已存在"); + } + throw e; + } + + // 5. 写入 sys_user_permission_category(如有) + if (pcIds != null && !pcIds.isEmpty()) { + for (Integer pcId : pcIds) { + SysUserPermissionCategory link = new SysUserPermissionCategory(); + link.setIUserId(user.getIIncrement()); + link.setIPermissionCategoryId(pcId); + link.setSGrantedBy(operatorUsername); + userPermissionCategoryMapper.insert(link); + } + } + + log.info("[user-create] username={} userCode={} byOperator={} permissionCount={}", + user.getSUsername(), user.getSUserCode(), operatorUsername, + pcIds == null ? 0 : pcIds.size()); + + return CreateUserVo.builder() + .userId(user.getIIncrement()) + .username(user.getSUsername()) + .userCode(user.getSUserCode()) + .build(); + } +} diff --git a/backend/src/test/java/com/xly/erp/module/usr/service/UserCreateServiceImplTest.java b/backend/src/test/java/com/xly/erp/module/usr/service/UserCreateServiceImplTest.java new file mode 100644 index 0000000..2ba9a6b --- /dev/null +++ b/backend/src/test/java/com/xly/erp/module/usr/service/UserCreateServiceImplTest.java @@ -0,0 +1,156 @@ +package com.xly.erp.module.usr.service; + +import com.xly.erp.common.exception.BizException; +import com.xly.erp.common.response.ErrorCode; +import com.xly.erp.module.usr.dto.CreateUserReq; +import com.xly.erp.module.usr.entity.SysUser; +import com.xly.erp.module.usr.entity.SysUserPermissionCategory; +import com.xly.erp.module.usr.mapper.SysUserMapper; +import com.xly.erp.module.usr.mapper.SysUserPermissionCategoryMapper; +import com.xly.erp.module.usr.support.LoginTestSeeder; +import com.xly.erp.module.usr.vo.CreateUserVo; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@ActiveProfiles("test") +class UserCreateServiceImplTest { + + @Autowired private UserCreateService service; + @Autowired private SysUserMapper userMapper; + @Autowired private SysUserPermissionCategoryMapper upcMapper; + @Autowired private BCryptPasswordEncoder encoder; + @Autowired private LoginTestSeeder seeder; + + private LoginTestSeeder.Fixture fx; + + @BeforeEach + void setUp() { + fx = seeder.reset(); + } + + private CreateUserReq buildReq(String username, String userCode) { + CreateUserReq r = new CreateUserReq(); + r.setUsername(username); + r.setUserCode(userCode); + r.setUserType("NORMAL"); + r.setLanguage("zh-CN"); + r.setCanEditDocument(false); + return r; + } + + // ===== 唯一性 / 外键校验(Task 7) ===== + + @Test + void create_usernameExists_throws40901() { + CreateUserReq r = buildReq(LoginTestSeeder.USER_OK, "U999"); + BizException e = assertThrows(BizException.class, () -> service.create(r, "admin")); + assertEquals(ErrorCode.CONFLICT_USERNAME, e.getCode()); + } + + @Test + void create_userCodeExists_throws40902() { + CreateUserReq r = buildReq("brandnew", "U001"); + BizException e = assertThrows(BizException.class, () -> service.create(r, "admin")); + assertEquals(ErrorCode.CONFLICT_USERCODE, e.getCode()); + } + + @Test + void create_employeeIdNotFound_throws40004() { + CreateUserReq r = buildReq("brandnew", "U999"); + r.setEmployeeId(99999); + BizException e = assertThrows(BizException.class, () -> service.create(r, "admin")); + assertEquals(ErrorCode.COMPANY_NOT_FOUND, e.getCode()); + } + + @Test + void create_permissionCategoryNotFound_throws40004() { + CreateUserReq r = buildReq("brandnew", "U999"); + List bad = new java.util.ArrayList<>(fx.activePermissionCategoryIds()); + bad.add(99999); + r.setPermissionCategoryIds(bad); + BizException e = assertThrows(BizException.class, () -> service.create(r, "admin")); + assertEquals(ErrorCode.COMPANY_NOT_FOUND, e.getCode()); + } + + @Test + void create_permissionCategorySoftDeleted_throws40004() { + CreateUserReq r = buildReq("brandnew", "U999"); + r.setPermissionCategoryIds(List.of(fx.deletedPermissionCategoryId())); + BizException e = assertThrows(BizException.class, () -> service.create(r, "admin")); + assertEquals(ErrorCode.COMPANY_NOT_FOUND, e.getCode()); + } + + // ===== 写入路径(Task 8) ===== + + @Test + void create_minimalFields_persistsUserWithInitialPassword() { + CreateUserReq r = buildReq("newbie", "U010"); + CreateUserVo vo = service.create(r, LoginTestSeeder.USER_ADMIN); + + assertNotNull(vo.getUserId()); + assertEquals("newbie", vo.getUsername()); + assertEquals("U010", vo.getUserCode()); + + SysUser db = userMapper.selectByUsername("newbie"); + assertNotNull(db); + assertEquals("NORMAL", db.getSUserType()); + assertEquals("zh-CN", db.getSLanguage()); + assertEquals(0, db.getICanEditDocument()); + assertEquals(0, db.getIIsDeleted()); + assertEquals(0, db.getIFailedLoginCount()); + assertTrue(encoder.matches("666666", db.getSPasswordHash()), + "初始密码必须 BCrypt 哈希为 666666"); + } + + @Test + void create_fullFields_persistsUserAndPermissionMappings() { + CreateUserReq r = buildReq("manager", "U020"); + r.setUserType("SUPER_ADMIN"); + r.setLanguage("en-US"); + r.setCanEditDocument(true); + r.setEmployeeId(fx.employeeId()); + r.setPermissionCategoryIds(fx.activePermissionCategoryIds()); + + CreateUserVo vo = service.create(r, LoginTestSeeder.USER_ADMIN); + + SysUser db = userMapper.selectByUsername("manager"); + assertNotNull(db); + assertEquals("SUPER_ADMIN", db.getSUserType()); + assertEquals("en-US", db.getSLanguage()); + assertEquals(1, db.getICanEditDocument()); + assertEquals(fx.employeeId(), db.getIEmployeeId()); + + List links = upcMapper.selectList( + new LambdaQueryWrapper() + .eq(SysUserPermissionCategory::getIUserId, vo.getUserId())); + assertEquals(fx.activePermissionCategoryIds().size(), links.size()); + for (SysUserPermissionCategory link : links) { + assertEquals(LoginTestSeeder.USER_ADMIN, link.getSGrantedBy()); + } + } + + @Test + void create_emptyPermissionCategories_persistsUserOnly() { + CreateUserReq r = buildReq("solo", "U030"); + r.setPermissionCategoryIds(List.of()); + CreateUserVo vo = service.create(r, LoginTestSeeder.USER_ADMIN); + + SysUser db = userMapper.selectByUsername("solo"); + assertNotNull(db); + + List links = upcMapper.selectList( + new LambdaQueryWrapper() + .eq(SysUserPermissionCategory::getIUserId, vo.getUserId())); + assertTrue(links.isEmpty()); + } +}