From fa2044fdb90576e3abbb8d79cc3576df88bdd06e Mon Sep 17 00:00:00 2001 From: zichun Date: Wed, 6 May 2026 17:57:53 +0800 Subject: [PATCH] feat(mod): delete module service REQ-MOD-003 --- backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java | 4 ++++ backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java | 40 ++++++++++++++++++++++++++++++++++++++++ backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 0 deletions(-) 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 index f482362..e628212 100644 --- 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 @@ -2,6 +2,7 @@ package com.xly.erp.module.mod.service; import com.xly.erp.module.mod.dto.ModuleCreateDTO; import com.xly.erp.module.mod.dto.ModuleUpdateDTO; +import com.xly.erp.module.mod.vo.ModuleDeleteResultVO; import com.xly.erp.module.mod.vo.ModuleVO; public interface ModuleService { @@ -10,4 +11,7 @@ public interface ModuleService { /** REQ-MOD-002 模块修改 */ ModuleVO update(Integer id, ModuleUpdateDTO dto); + + /** REQ-MOD-003 模块软删除 */ + ModuleDeleteResultVO delete(Integer id); } 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 index fcc27af..28a8c35 100644 --- 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 @@ -1,6 +1,7 @@ package com.xly.erp.module.mod.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.xly.erp.common.exception.BizException; import com.xly.erp.common.response.ErrorCode; import com.xly.erp.module.mod.dto.ModuleCreateDTO; @@ -8,6 +9,7 @@ import com.xly.erp.module.mod.dto.ModuleUpdateDTO; 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.ModuleDeleteResultVO; import com.xly.erp.module.mod.vo.ModuleVO; import lombok.RequiredArgsConstructor; import org.springframework.dao.DuplicateKeyException; @@ -124,4 +126,42 @@ public class ModuleServiceImpl implements ModuleService { // 5. 返回 VO return ModuleVO.from(target); } + + /** REQ-MOD-003 模块软删除 */ + @Override + @Transactional(rollbackFor = Exception.class) + public ModuleDeleteResultVO delete(Integer id) { + // 1. 目标存在 + 未软删除 + ModuleEntity target = moduleMapper.selectById(id); + if (target == null || Boolean.TRUE.equals(target.getBDeleted())) { + throw new BizException(ErrorCode.MOD_NOT_FOUND); + } + + // 2. 子模块(未软删除)引用检查 + Long childCount = moduleMapper.selectCount( + new LambdaQueryWrapper() + .eq(ModuleEntity::getIParentId, id) + .eq(ModuleEntity::getBDeleted, false)); + if (childCount != null && childCount > 0L) { + throw new BizException(ErrorCode.MOD_HAS_REFERENCES); + } + + // 3. 软删除写入:仅触碰三件套,避免覆盖其他字段;并以 bDeleted=0 兜底并发 + ModuleEntity patch = new ModuleEntity(); + patch.setIIncrement(id); + patch.setBDeleted(true); + patch.setTDeletedDate(LocalDateTime.now()); + // sDeletedBy 留 null(FieldStrategy 默认 NOT_NULL 跳过;REQ-USR-004 后由登录上下文回填) + + int affected = moduleMapper.update(patch, + new LambdaUpdateWrapper() + .eq(ModuleEntity::getIIncrement, id) + .eq(ModuleEntity::getBDeleted, false)); + if (affected == 0) { + // 校验后被并发删除 + throw new BizException(ErrorCode.MOD_NOT_FOUND); + } + + return ModuleDeleteResultVO.of(id, true); + } } 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 index eff23ba..3179596 100644 --- 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 @@ -8,6 +8,7 @@ import com.xly.erp.module.mod.dto.ModuleUpdateDTO; 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.ModuleDeleteResultVO; import com.xly.erp.module.mod.vo.ModuleVO; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -309,4 +310,88 @@ class ModuleServiceImplTest { verify(moduleMapper).updateById(cap.capture()); assertThat(cap.getValue().getIParentId()).isNull(); } + + // ============================================================ + // REQ-MOD-003 delete 系列 + // ============================================================ + + @Test + void delete_targetNotFound_throws40421() { + when(moduleMapper.selectById(20)).thenReturn(null); + + assertThatThrownBy(() -> service.delete(20)) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.MOD_NOT_FOUND.getCode()); + } + + @Test + void delete_targetAlreadyDeleted_throws40421() { + ModuleEntity t = existingTarget(21); + t.setBDeleted(true); + when(moduleMapper.selectById(21)).thenReturn(t); + + assertThatThrownBy(() -> service.delete(21)) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.MOD_NOT_FOUND.getCode()); + } + + @Test + void delete_hasUndeletedChildren_throws40912() { + ModuleEntity t = existingTarget(22); + when(moduleMapper.selectById(22)).thenReturn(t); + when(moduleMapper.selectCount(any(Wrapper.class))).thenReturn(3L); + + assertThatThrownBy(() -> service.delete(22)) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.MOD_HAS_REFERENCES.getCode()); + } + + @Test + void delete_leafModule_writesSoftDeleteFields_returnsResult() { + ModuleEntity t = existingTarget(23); + when(moduleMapper.selectById(23)).thenReturn(t); + when(moduleMapper.selectCount(any(Wrapper.class))).thenReturn(0L); + when(moduleMapper.update((ModuleEntity) any(), (Wrapper) any())).thenReturn(1); + + ModuleDeleteResultVO vo = service.delete(23); + + assertThat(vo.getIIncrement()).isEqualTo(23); + assertThat(vo.getBDeleted()).isTrue(); + + ArgumentCaptor cap = ArgumentCaptor.forClass(ModuleEntity.class); + verify(moduleMapper).update(cap.capture(), (Wrapper) any()); + ModuleEntity patch = cap.getValue(); + assertThat(patch.getIIncrement()).isEqualTo(23); + assertThat(patch.getBDeleted()).isTrue(); + assertThat(patch.getTDeletedDate()).isNotNull(); + } + + @Test + void delete_softDeletedChildren_doesNotBlock() { + ModuleEntity t = existingTarget(24); + when(moduleMapper.selectById(24)).thenReturn(t); + // 子全部已软删除 → selectCount(bDeleted=0 过滤) 返回 0 + when(moduleMapper.selectCount(any(Wrapper.class))).thenReturn(0L); + when(moduleMapper.update((ModuleEntity) any(), (Wrapper) any())).thenReturn(1); + + ModuleDeleteResultVO vo = service.delete(24); + assertThat(vo.getBDeleted()).isTrue(); + } + + @Test + void delete_concurrentRace_throws40421() { + ModuleEntity t = existingTarget(25); + when(moduleMapper.selectById(25)).thenReturn(t); + when(moduleMapper.selectCount(any(Wrapper.class))).thenReturn(0L); + // update 影响行数 0 → 视为并发删除 + when(moduleMapper.update((ModuleEntity) any(), (Wrapper) any())).thenReturn(0); + + assertThatThrownBy(() -> service.delete(25)) + .isInstanceOf(BizException.class) + .extracting(e -> ((BizException) e).getCode()) + .isEqualTo(ErrorCode.MOD_NOT_FOUND.getCode()); + } } -- libgit2 0.22.2