From 74a4d485b7c13a35ed8427c6ceb037b5b386e032 Mon Sep 17 00:00:00 2001 From: zichun Date: Wed, 6 May 2026 17:25:16 +0800 Subject: [PATCH] feat(mod): create module service REQ-MOD-001 --- backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java | 8 ++++++++ backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+), 0 deletions(-) create mode 100644 backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java create mode 100644 backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java create mode 100644 backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java diff --git a/backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java b/backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java new file mode 100644 index 0000000..d893d62 --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java @@ -0,0 +1,8 @@ +package com.xly.erp.module.mod.service; + +import com.xly.erp.module.mod.dto.ModuleCreateDTO; +import com.xly.erp.module.mod.vo.ModuleVO; + +public interface ModuleService { + ModuleVO create(ModuleCreateDTO dto); +} diff --git a/backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java b/backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java new file mode 100644 index 0000000..4e13c69 --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java @@ -0,0 +1,68 @@ +package com.xly.erp.module.mod.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.xly.erp.common.exception.BizException; +import com.xly.erp.common.response.ErrorCode; +import com.xly.erp.module.mod.dto.ModuleCreateDTO; +import com.xly.erp.module.mod.entity.ModuleEntity; +import com.xly.erp.module.mod.mapper.ModuleMapper; +import com.xly.erp.module.mod.service.ModuleService; +import com.xly.erp.module.mod.vo.ModuleVO; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class ModuleServiceImpl implements ModuleService { + + private final ModuleMapper moduleMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public ModuleVO create(ModuleCreateDTO dto) { + // 1. 父模块校验(仅当 iParentId 非空) + if (dto.getIParentId() != null) { + ModuleEntity parent = moduleMapper.selectById(dto.getIParentId()); + if (parent == null || Boolean.TRUE.equals(parent.getBDeleted())) { + throw new BizException(ErrorCode.MOD_PARENT_NOT_FOUND); + } + } + + // 2. sProcedureName 唯一性预检(未软删除范围) + Long exist = moduleMapper.selectCount( + new LambdaQueryWrapper() + .eq(ModuleEntity::getSProcedureName, dto.getSProcedureName()) + .eq(ModuleEntity::getBDeleted, false)); + if (exist != null && exist > 0L) { + throw new BizException(ErrorCode.MOD_PROC_NAME_DUP); + } + + // 3. 构造 entity + ModuleEntity e = new ModuleEntity(); + e.setSDisplayType(dto.getSDisplayType()); + e.setSProcedureName(dto.getSProcedureName()); + e.setSModuleType(dto.getSModuleType()); + e.setSManageDeptEn(dto.getSManageDeptEn()); + e.setBShowPermission(dto.getBShowPermission() != null ? dto.getBShowPermission() : Boolean.FALSE); + e.setSModuleNameZh(dto.getSModuleNameZh()); + e.setIParentId(dto.getIParentId()); + e.setISortOrder(dto.getISortOrder() != null ? dto.getISortOrder() : 0); + e.setTCreateDate(LocalDateTime.now()); + e.setBDeleted(Boolean.FALSE); + // sId / sBrandsId / sSubsidiaryId / sCreatedBy / tDeletedDate / sDeletedBy 留 null(REQ-USR-004 后由登录上下文 / 多租户上下文回填) + + // 4. 插入;并发下唯一约束兜底 + try { + moduleMapper.insert(e); + } catch (DuplicateKeyException dup) { + throw new BizException(ErrorCode.MOD_PROC_NAME_DUP); + } + + // 5. 返回 VO + return ModuleVO.from(e); + } +} diff --git a/backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java b/backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java new file mode 100644 index 0000000..f3ae5a6 --- /dev/null +++ b/backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java @@ -0,0 +1,135 @@ +package com.xly.erp.module.mod.service; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.xly.erp.common.exception.BizException; +import com.xly.erp.common.response.ErrorCode; +import com.xly.erp.module.mod.dto.ModuleCreateDTO; +import com.xly.erp.module.mod.entity.ModuleEntity; +import com.xly.erp.module.mod.mapper.ModuleMapper; +import com.xly.erp.module.mod.service.impl.ModuleServiceImpl; +import com.xly.erp.module.mod.vo.ModuleVO; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.DuplicateKeyException; + +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.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ModuleServiceImplTest { + + @Mock ModuleMapper moduleMapper; + + @InjectMocks ModuleServiceImpl service; + + private ModuleCreateDTO baseDto() { + ModuleCreateDTO d = new ModuleCreateDTO(); + d.setSDisplayType("前端业务"); + d.setSProcedureName("sp_audit_user"); + d.setSModuleType("USR"); + d.setSManageDeptEn("IT"); + d.setBShowPermission(false); + d.setSModuleNameZh("用户管理"); + return d; + } + + @Test + void create_rootModule_returnsVOWithGeneratedId() { + when(moduleMapper.selectCount(any(Wrapper.class))).thenReturn(0L); + when(moduleMapper.insert((ModuleEntity) any())).thenAnswer(inv -> { + ModuleEntity e = inv.getArgument(0); + e.setIIncrement(123); + return 1; + }); + + ModuleVO vo = service.create(baseDto()); + + assertThat(vo.getIIncrement()).isEqualTo(123); + assertThat(vo.getSProcedureName()).isEqualTo("sp_audit_user"); + assertThat(vo.getBShowPermission()).isFalse(); + assertThat(vo.getISortOrder()).isZero(); + assertThat(vo.getBDeleted()).isFalse(); + assertThat(vo.getTCreateDate()).isNotNull(); + } + + @Test + void create_childModule_validatesParentExists() { + ModuleCreateDTO d = baseDto(); + d.setIParentId(7); + + ModuleEntity parent = new ModuleEntity(); + parent.setIIncrement(7); + parent.setBDeleted(false); + when(moduleMapper.selectById(7)).thenReturn(parent); + when(moduleMapper.selectCount(any(Wrapper.class))).thenReturn(0L); + when(moduleMapper.insert((ModuleEntity) any())).thenAnswer(inv -> { + ModuleEntity e = inv.getArgument(0); + e.setIIncrement(8); + return 1; + }); + + ModuleVO vo = service.create(d); + assertThat(vo.getIIncrement()).isEqualTo(8); + assertThat(vo.getIParentId()).isEqualTo(7); + } + + @Test + void create_parentNotFound_throwsBizException40411() { + ModuleCreateDTO d = baseDto(); + d.setIParentId(999999); + when(moduleMapper.selectById(999999)).thenReturn(null); + + assertThatThrownBy(() -> service.create(d)) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.MOD_PARENT_NOT_FOUND.getCode()); + verify(moduleMapper, never()).insert((ModuleEntity) any()); + } + + @Test + void create_parentSoftDeleted_throwsBizException40411() { + ModuleCreateDTO d = baseDto(); + d.setIParentId(5); + + ModuleEntity deleted = new ModuleEntity(); + deleted.setIIncrement(5); + deleted.setBDeleted(true); + when(moduleMapper.selectById(5)).thenReturn(deleted); + + assertThatThrownBy(() -> service.create(d)) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.MOD_PARENT_NOT_FOUND.getCode()); + } + + @Test + void create_duplicateProcedureName_preCheck_throwsBizException40911() { + when(moduleMapper.selectCount(any(Wrapper.class))).thenReturn(1L); + + assertThatThrownBy(() -> service.create(baseDto())) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.MOD_PROC_NAME_DUP.getCode()); + verify(moduleMapper, never()).insert((ModuleEntity) any()); + } + + @Test + void create_duplicateProcedureName_concurrentInsert_throwsBizException40911() { + when(moduleMapper.selectCount(any(Wrapper.class))).thenReturn(0L); + when(moduleMapper.insert((ModuleEntity) any())) + .thenThrow(new DuplicateKeyException("uk_procedure_name")); + + assertThatThrownBy(() -> service.create(baseDto())) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.MOD_PROC_NAME_DUP.getCode()); + } +} -- libgit2 0.22.2