Commit 83a1ea9c89b2a9537d355e3465b418f00181e367

Authored by zichun
1 parent bc40a751

feat(usr): UserCreateService 校验 + 写入 + 权限分类授权 REQ-USR-002

backend/src/main/java/com/xly/erp/module/usr/service/UserCreateService.java 0 → 100644
  1 +package com.xly.erp.module.usr.service;
  2 +
  3 +import com.xly.erp.module.usr.dto.CreateUserReq;
  4 +import com.xly.erp.module.usr.vo.CreateUserVo;
  5 +
  6 +public interface UserCreateService {
  7 + /**
  8 + * 新建用户 + 权限分类授权。
  9 + * REQ-USR-002。
  10 + *
  11 + * @param req 已通过 jakarta 校验的请求体
  12 + * @param operatorUsername 当前登录用户(写入 sCreatedBy / sGrantedBy)
  13 + * @throws com.xly.erp.common.exception.BizException
  14 + * 40004 employee / permissionCategory 不存在 / 40901 用户名重复 / 40902 用户号重复
  15 + */
  16 + CreateUserVo create(CreateUserReq req, String operatorUsername);
  17 +}
... ...
backend/src/main/java/com/xly/erp/module/usr/service/impl/UserCreateServiceImpl.java 0 → 100644
  1 +package com.xly.erp.module.usr.service.impl;
  2 +
  3 +import com.xly.erp.common.exception.BizException;
  4 +import com.xly.erp.common.response.ErrorCode;
  5 +import com.xly.erp.module.usr.dto.CreateUserReq;
  6 +import com.xly.erp.module.usr.entity.SysEmployee;
  7 +import com.xly.erp.module.usr.entity.SysUser;
  8 +import com.xly.erp.module.usr.entity.SysUserPermissionCategory;
  9 +import com.xly.erp.module.usr.mapper.SysEmployeeMapper;
  10 +import com.xly.erp.module.usr.mapper.SysPermissionCategoryMapper;
  11 +import com.xly.erp.module.usr.mapper.SysUserMapper;
  12 +import com.xly.erp.module.usr.mapper.SysUserPermissionCategoryMapper;
  13 +import com.xly.erp.module.usr.service.UserCreateService;
  14 +import com.xly.erp.module.usr.vo.CreateUserVo;
  15 +import lombok.RequiredArgsConstructor;
  16 +import lombok.extern.slf4j.Slf4j;
  17 +import org.springframework.dao.DataIntegrityViolationException;
  18 +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  19 +import org.springframework.stereotype.Service;
  20 +import org.springframework.transaction.annotation.Transactional;
  21 +
  22 +import java.util.List;
  23 +
  24 +@Service
  25 +@RequiredArgsConstructor
  26 +@Slf4j
  27 +public class UserCreateServiceImpl implements UserCreateService {
  28 +
  29 + static final String INITIAL_PASSWORD = "666666";
  30 +
  31 + private final SysUserMapper userMapper;
  32 + private final SysEmployeeMapper employeeMapper;
  33 + private final SysPermissionCategoryMapper permissionCategoryMapper;
  34 + private final SysUserPermissionCategoryMapper userPermissionCategoryMapper;
  35 + private final BCryptPasswordEncoder passwordEncoder;
  36 +
  37 + @Override
  38 + @Transactional
  39 + public CreateUserVo create(CreateUserReq req, String operatorUsername) {
  40 + // 1. 唯一性预检(返友好错误码;DB 唯一索引兜底并发场景)
  41 + if (userMapper.existsByUsername(req.getUsername())) {
  42 + throw new BizException(ErrorCode.CONFLICT_USERNAME, "用户名已存在");
  43 + }
  44 + if (userMapper.existsByUserCode(req.getUserCode())) {
  45 + throw new BizException(ErrorCode.CONFLICT_USERCODE, "用户号已存在");
  46 + }
  47 +
  48 + // 2. employee 外键校验
  49 + if (req.getEmployeeId() != null) {
  50 + SysEmployee emp = employeeMapper.selectById(req.getEmployeeId());
  51 + if (emp == null || Integer.valueOf(1).equals(emp.getIIsDeleted())) {
  52 + throw new BizException(ErrorCode.COMPANY_NOT_FOUND, "指定的员工不存在或已删除");
  53 + }
  54 + }
  55 +
  56 + // 3. permissionCategory 外键校验(批量)
  57 + List<Integer> pcIds = req.getPermissionCategoryIds();
  58 + if (pcIds != null && !pcIds.isEmpty()) {
  59 + int active = permissionCategoryMapper.countActiveByIds(pcIds);
  60 + if (active != pcIds.size()) {
  61 + throw new BizException(ErrorCode.COMPANY_NOT_FOUND,
  62 + "指定的权限分类含不存在或已删除项");
  63 + }
  64 + }
  65 +
  66 + // 4. 写入 sys_user
  67 + SysUser user = new SysUser();
  68 + user.setSUsername(req.getUsername());
  69 + user.setSUserCode(req.getUserCode());
  70 + user.setSPasswordHash(passwordEncoder.encode(INITIAL_PASSWORD));
  71 + user.setIEmployeeId(req.getEmployeeId());
  72 + user.setSUserType(req.getUserType());
  73 + user.setSLanguage(req.getLanguage());
  74 + user.setICanEditDocument(Boolean.TRUE.equals(req.getCanEditDocument()) ? 1 : 0);
  75 + user.setIIsDeleted(0);
  76 + user.setIFailedLoginCount(0);
  77 + user.setSCreatedBy(operatorUsername);
  78 + try {
  79 + userMapper.insert(user);
  80 + } catch (DataIntegrityViolationException e) {
  81 + String msg = e.getMessage() == null ? "" : e.getMessage();
  82 + if (msg.contains("uk_sys_user_username")) {
  83 + throw new BizException(ErrorCode.CONFLICT_USERNAME, "用户名已存在");
  84 + }
  85 + if (msg.contains("uk_sys_user_code")) {
  86 + throw new BizException(ErrorCode.CONFLICT_USERCODE, "用户号已存在");
  87 + }
  88 + throw e;
  89 + }
  90 +
  91 + // 5. 写入 sys_user_permission_category(如有)
  92 + if (pcIds != null && !pcIds.isEmpty()) {
  93 + for (Integer pcId : pcIds) {
  94 + SysUserPermissionCategory link = new SysUserPermissionCategory();
  95 + link.setIUserId(user.getIIncrement());
  96 + link.setIPermissionCategoryId(pcId);
  97 + link.setSGrantedBy(operatorUsername);
  98 + userPermissionCategoryMapper.insert(link);
  99 + }
  100 + }
  101 +
  102 + log.info("[user-create] username={} userCode={} byOperator={} permissionCount={}",
  103 + user.getSUsername(), user.getSUserCode(), operatorUsername,
  104 + pcIds == null ? 0 : pcIds.size());
  105 +
  106 + return CreateUserVo.builder()
  107 + .userId(user.getIIncrement())
  108 + .username(user.getSUsername())
  109 + .userCode(user.getSUserCode())
  110 + .build();
  111 + }
  112 +}
... ...
backend/src/test/java/com/xly/erp/module/usr/service/UserCreateServiceImplTest.java 0 → 100644
  1 +package com.xly.erp.module.usr.service;
  2 +
  3 +import com.xly.erp.common.exception.BizException;
  4 +import com.xly.erp.common.response.ErrorCode;
  5 +import com.xly.erp.module.usr.dto.CreateUserReq;
  6 +import com.xly.erp.module.usr.entity.SysUser;
  7 +import com.xly.erp.module.usr.entity.SysUserPermissionCategory;
  8 +import com.xly.erp.module.usr.mapper.SysUserMapper;
  9 +import com.xly.erp.module.usr.mapper.SysUserPermissionCategoryMapper;
  10 +import com.xly.erp.module.usr.support.LoginTestSeeder;
  11 +import com.xly.erp.module.usr.vo.CreateUserVo;
  12 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  13 +import org.junit.jupiter.api.BeforeEach;
  14 +import org.junit.jupiter.api.Test;
  15 +import org.springframework.beans.factory.annotation.Autowired;
  16 +import org.springframework.boot.test.context.SpringBootTest;
  17 +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  18 +import org.springframework.test.context.ActiveProfiles;
  19 +
  20 +import java.util.List;
  21 +
  22 +import static org.junit.jupiter.api.Assertions.*;
  23 +
  24 +@SpringBootTest
  25 +@ActiveProfiles("test")
  26 +class UserCreateServiceImplTest {
  27 +
  28 + @Autowired private UserCreateService service;
  29 + @Autowired private SysUserMapper userMapper;
  30 + @Autowired private SysUserPermissionCategoryMapper upcMapper;
  31 + @Autowired private BCryptPasswordEncoder encoder;
  32 + @Autowired private LoginTestSeeder seeder;
  33 +
  34 + private LoginTestSeeder.Fixture fx;
  35 +
  36 + @BeforeEach
  37 + void setUp() {
  38 + fx = seeder.reset();
  39 + }
  40 +
  41 + private CreateUserReq buildReq(String username, String userCode) {
  42 + CreateUserReq r = new CreateUserReq();
  43 + r.setUsername(username);
  44 + r.setUserCode(userCode);
  45 + r.setUserType("NORMAL");
  46 + r.setLanguage("zh-CN");
  47 + r.setCanEditDocument(false);
  48 + return r;
  49 + }
  50 +
  51 + // ===== 唯一性 / 外键校验(Task 7) =====
  52 +
  53 + @Test
  54 + void create_usernameExists_throws40901() {
  55 + CreateUserReq r = buildReq(LoginTestSeeder.USER_OK, "U999");
  56 + BizException e = assertThrows(BizException.class, () -> service.create(r, "admin"));
  57 + assertEquals(ErrorCode.CONFLICT_USERNAME, e.getCode());
  58 + }
  59 +
  60 + @Test
  61 + void create_userCodeExists_throws40902() {
  62 + CreateUserReq r = buildReq("brandnew", "U001");
  63 + BizException e = assertThrows(BizException.class, () -> service.create(r, "admin"));
  64 + assertEquals(ErrorCode.CONFLICT_USERCODE, e.getCode());
  65 + }
  66 +
  67 + @Test
  68 + void create_employeeIdNotFound_throws40004() {
  69 + CreateUserReq r = buildReq("brandnew", "U999");
  70 + r.setEmployeeId(99999);
  71 + BizException e = assertThrows(BizException.class, () -> service.create(r, "admin"));
  72 + assertEquals(ErrorCode.COMPANY_NOT_FOUND, e.getCode());
  73 + }
  74 +
  75 + @Test
  76 + void create_permissionCategoryNotFound_throws40004() {
  77 + CreateUserReq r = buildReq("brandnew", "U999");
  78 + List<Integer> bad = new java.util.ArrayList<>(fx.activePermissionCategoryIds());
  79 + bad.add(99999);
  80 + r.setPermissionCategoryIds(bad);
  81 + BizException e = assertThrows(BizException.class, () -> service.create(r, "admin"));
  82 + assertEquals(ErrorCode.COMPANY_NOT_FOUND, e.getCode());
  83 + }
  84 +
  85 + @Test
  86 + void create_permissionCategorySoftDeleted_throws40004() {
  87 + CreateUserReq r = buildReq("brandnew", "U999");
  88 + r.setPermissionCategoryIds(List.of(fx.deletedPermissionCategoryId()));
  89 + BizException e = assertThrows(BizException.class, () -> service.create(r, "admin"));
  90 + assertEquals(ErrorCode.COMPANY_NOT_FOUND, e.getCode());
  91 + }
  92 +
  93 + // ===== 写入路径(Task 8) =====
  94 +
  95 + @Test
  96 + void create_minimalFields_persistsUserWithInitialPassword() {
  97 + CreateUserReq r = buildReq("newbie", "U010");
  98 + CreateUserVo vo = service.create(r, LoginTestSeeder.USER_ADMIN);
  99 +
  100 + assertNotNull(vo.getUserId());
  101 + assertEquals("newbie", vo.getUsername());
  102 + assertEquals("U010", vo.getUserCode());
  103 +
  104 + SysUser db = userMapper.selectByUsername("newbie");
  105 + assertNotNull(db);
  106 + assertEquals("NORMAL", db.getSUserType());
  107 + assertEquals("zh-CN", db.getSLanguage());
  108 + assertEquals(0, db.getICanEditDocument());
  109 + assertEquals(0, db.getIIsDeleted());
  110 + assertEquals(0, db.getIFailedLoginCount());
  111 + assertTrue(encoder.matches("666666", db.getSPasswordHash()),
  112 + "初始密码必须 BCrypt 哈希为 666666");
  113 + }
  114 +
  115 + @Test
  116 + void create_fullFields_persistsUserAndPermissionMappings() {
  117 + CreateUserReq r = buildReq("manager", "U020");
  118 + r.setUserType("SUPER_ADMIN");
  119 + r.setLanguage("en-US");
  120 + r.setCanEditDocument(true);
  121 + r.setEmployeeId(fx.employeeId());
  122 + r.setPermissionCategoryIds(fx.activePermissionCategoryIds());
  123 +
  124 + CreateUserVo vo = service.create(r, LoginTestSeeder.USER_ADMIN);
  125 +
  126 + SysUser db = userMapper.selectByUsername("manager");
  127 + assertNotNull(db);
  128 + assertEquals("SUPER_ADMIN", db.getSUserType());
  129 + assertEquals("en-US", db.getSLanguage());
  130 + assertEquals(1, db.getICanEditDocument());
  131 + assertEquals(fx.employeeId(), db.getIEmployeeId());
  132 +
  133 + List<SysUserPermissionCategory> links = upcMapper.selectList(
  134 + new LambdaQueryWrapper<SysUserPermissionCategory>()
  135 + .eq(SysUserPermissionCategory::getIUserId, vo.getUserId()));
  136 + assertEquals(fx.activePermissionCategoryIds().size(), links.size());
  137 + for (SysUserPermissionCategory link : links) {
  138 + assertEquals(LoginTestSeeder.USER_ADMIN, link.getSGrantedBy());
  139 + }
  140 + }
  141 +
  142 + @Test
  143 + void create_emptyPermissionCategories_persistsUserOnly() {
  144 + CreateUserReq r = buildReq("solo", "U030");
  145 + r.setPermissionCategoryIds(List.of());
  146 + CreateUserVo vo = service.create(r, LoginTestSeeder.USER_ADMIN);
  147 +
  148 + SysUser db = userMapper.selectByUsername("solo");
  149 + assertNotNull(db);
  150 +
  151 + List<SysUserPermissionCategory> links = upcMapper.selectList(
  152 + new LambdaQueryWrapper<SysUserPermissionCategory>()
  153 + .eq(SysUserPermissionCategory::getIUserId, vo.getUserId()));
  154 + assertTrue(links.isEmpty());
  155 + }
  156 +}
... ...