Commit fa2044fdb90576e3abbb8d79cc3576df88bdd06e

Authored by zichun
1 parent 5379f491

feat(mod): delete module service REQ-MOD-003

backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java
@@ -2,6 +2,7 @@ package com.xly.erp.module.mod.service; @@ -2,6 +2,7 @@ package com.xly.erp.module.mod.service;
2 2
3 import com.xly.erp.module.mod.dto.ModuleCreateDTO; 3 import com.xly.erp.module.mod.dto.ModuleCreateDTO;
4 import com.xly.erp.module.mod.dto.ModuleUpdateDTO; 4 import com.xly.erp.module.mod.dto.ModuleUpdateDTO;
  5 +import com.xly.erp.module.mod.vo.ModuleDeleteResultVO;
5 import com.xly.erp.module.mod.vo.ModuleVO; 6 import com.xly.erp.module.mod.vo.ModuleVO;
6 7
7 public interface ModuleService { 8 public interface ModuleService {
@@ -10,4 +11,7 @@ public interface ModuleService { @@ -10,4 +11,7 @@ public interface ModuleService {
10 11
11 /** REQ-MOD-002 模块修改 */ 12 /** REQ-MOD-002 模块修改 */
12 ModuleVO update(Integer id, ModuleUpdateDTO dto); 13 ModuleVO update(Integer id, ModuleUpdateDTO dto);
  14 +
  15 + /** REQ-MOD-003 模块软删除 */
  16 + ModuleDeleteResultVO delete(Integer id);
13 } 17 }
backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java
1 package com.xly.erp.module.mod.service.impl; 1 package com.xly.erp.module.mod.service.impl;
2 2
3 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 3 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
4 import com.xly.erp.common.exception.BizException; 5 import com.xly.erp.common.exception.BizException;
5 import com.xly.erp.common.response.ErrorCode; 6 import com.xly.erp.common.response.ErrorCode;
6 import com.xly.erp.module.mod.dto.ModuleCreateDTO; 7 import com.xly.erp.module.mod.dto.ModuleCreateDTO;
@@ -8,6 +9,7 @@ import com.xly.erp.module.mod.dto.ModuleUpdateDTO; @@ -8,6 +9,7 @@ import com.xly.erp.module.mod.dto.ModuleUpdateDTO;
8 import com.xly.erp.module.mod.entity.ModuleEntity; 9 import com.xly.erp.module.mod.entity.ModuleEntity;
9 import com.xly.erp.module.mod.mapper.ModuleMapper; 10 import com.xly.erp.module.mod.mapper.ModuleMapper;
10 import com.xly.erp.module.mod.service.ModuleService; 11 import com.xly.erp.module.mod.service.ModuleService;
  12 +import com.xly.erp.module.mod.vo.ModuleDeleteResultVO;
11 import com.xly.erp.module.mod.vo.ModuleVO; 13 import com.xly.erp.module.mod.vo.ModuleVO;
12 import lombok.RequiredArgsConstructor; 14 import lombok.RequiredArgsConstructor;
13 import org.springframework.dao.DuplicateKeyException; 15 import org.springframework.dao.DuplicateKeyException;
@@ -124,4 +126,42 @@ public class ModuleServiceImpl implements ModuleService { @@ -124,4 +126,42 @@ public class ModuleServiceImpl implements ModuleService {
124 // 5. 返回 VO 126 // 5. 返回 VO
125 return ModuleVO.from(target); 127 return ModuleVO.from(target);
126 } 128 }
  129 +
  130 + /** REQ-MOD-003 模块软删除 */
  131 + @Override
  132 + @Transactional(rollbackFor = Exception.class)
  133 + public ModuleDeleteResultVO delete(Integer id) {
  134 + // 1. 目标存在 + 未软删除
  135 + ModuleEntity target = moduleMapper.selectById(id);
  136 + if (target == null || Boolean.TRUE.equals(target.getBDeleted())) {
  137 + throw new BizException(ErrorCode.MOD_NOT_FOUND);
  138 + }
  139 +
  140 + // 2. 子模块(未软删除)引用检查
  141 + Long childCount = moduleMapper.selectCount(
  142 + new LambdaQueryWrapper<ModuleEntity>()
  143 + .eq(ModuleEntity::getIParentId, id)
  144 + .eq(ModuleEntity::getBDeleted, false));
  145 + if (childCount != null && childCount > 0L) {
  146 + throw new BizException(ErrorCode.MOD_HAS_REFERENCES);
  147 + }
  148 +
  149 + // 3. 软删除写入:仅触碰三件套,避免覆盖其他字段;并以 bDeleted=0 兜底并发
  150 + ModuleEntity patch = new ModuleEntity();
  151 + patch.setIIncrement(id);
  152 + patch.setBDeleted(true);
  153 + patch.setTDeletedDate(LocalDateTime.now());
  154 + // sDeletedBy 留 null(FieldStrategy 默认 NOT_NULL 跳过;REQ-USR-004 后由登录上下文回填)
  155 +
  156 + int affected = moduleMapper.update(patch,
  157 + new LambdaUpdateWrapper<ModuleEntity>()
  158 + .eq(ModuleEntity::getIIncrement, id)
  159 + .eq(ModuleEntity::getBDeleted, false));
  160 + if (affected == 0) {
  161 + // 校验后被并发删除
  162 + throw new BizException(ErrorCode.MOD_NOT_FOUND);
  163 + }
  164 +
  165 + return ModuleDeleteResultVO.of(id, true);
  166 + }
127 } 167 }
backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java
@@ -8,6 +8,7 @@ import com.xly.erp.module.mod.dto.ModuleUpdateDTO; @@ -8,6 +8,7 @@ import com.xly.erp.module.mod.dto.ModuleUpdateDTO;
8 import com.xly.erp.module.mod.entity.ModuleEntity; 8 import com.xly.erp.module.mod.entity.ModuleEntity;
9 import com.xly.erp.module.mod.mapper.ModuleMapper; 9 import com.xly.erp.module.mod.mapper.ModuleMapper;
10 import com.xly.erp.module.mod.service.impl.ModuleServiceImpl; 10 import com.xly.erp.module.mod.service.impl.ModuleServiceImpl;
  11 +import com.xly.erp.module.mod.vo.ModuleDeleteResultVO;
11 import com.xly.erp.module.mod.vo.ModuleVO; 12 import com.xly.erp.module.mod.vo.ModuleVO;
12 import org.junit.jupiter.api.Test; 13 import org.junit.jupiter.api.Test;
13 import org.junit.jupiter.api.extension.ExtendWith; 14 import org.junit.jupiter.api.extension.ExtendWith;
@@ -309,4 +310,88 @@ class ModuleServiceImplTest { @@ -309,4 +310,88 @@ class ModuleServiceImplTest {
309 verify(moduleMapper).updateById(cap.capture()); 310 verify(moduleMapper).updateById(cap.capture());
310 assertThat(cap.getValue().getIParentId()).isNull(); 311 assertThat(cap.getValue().getIParentId()).isNull();
311 } 312 }
  313 +
  314 + // ============================================================
  315 + // REQ-MOD-003 delete 系列
  316 + // ============================================================
  317 +
  318 + @Test
  319 + void delete_targetNotFound_throws40421() {
  320 + when(moduleMapper.selectById(20)).thenReturn(null);
  321 +
  322 + assertThatThrownBy(() -> service.delete(20))
  323 + .isInstanceOf(BizException.class)
  324 + .extracting(e -> ((BizException) e).getCode())
  325 + .isEqualTo(ErrorCode.MOD_NOT_FOUND.getCode());
  326 + }
  327 +
  328 + @Test
  329 + void delete_targetAlreadyDeleted_throws40421() {
  330 + ModuleEntity t = existingTarget(21);
  331 + t.setBDeleted(true);
  332 + when(moduleMapper.selectById(21)).thenReturn(t);
  333 +
  334 + assertThatThrownBy(() -> service.delete(21))
  335 + .isInstanceOf(BizException.class)
  336 + .extracting(e -> ((BizException) e).getCode())
  337 + .isEqualTo(ErrorCode.MOD_NOT_FOUND.getCode());
  338 + }
  339 +
  340 + @Test
  341 + void delete_hasUndeletedChildren_throws40912() {
  342 + ModuleEntity t = existingTarget(22);
  343 + when(moduleMapper.selectById(22)).thenReturn(t);
  344 + when(moduleMapper.selectCount(any(Wrapper.class))).thenReturn(3L);
  345 +
  346 + assertThatThrownBy(() -> service.delete(22))
  347 + .isInstanceOf(BizException.class)
  348 + .extracting(e -> ((BizException) e).getCode())
  349 + .isEqualTo(ErrorCode.MOD_HAS_REFERENCES.getCode());
  350 + }
  351 +
  352 + @Test
  353 + void delete_leafModule_writesSoftDeleteFields_returnsResult() {
  354 + ModuleEntity t = existingTarget(23);
  355 + when(moduleMapper.selectById(23)).thenReturn(t);
  356 + when(moduleMapper.selectCount(any(Wrapper.class))).thenReturn(0L);
  357 + when(moduleMapper.update((ModuleEntity) any(), (Wrapper<ModuleEntity>) any())).thenReturn(1);
  358 +
  359 + ModuleDeleteResultVO vo = service.delete(23);
  360 +
  361 + assertThat(vo.getIIncrement()).isEqualTo(23);
  362 + assertThat(vo.getBDeleted()).isTrue();
  363 +
  364 + ArgumentCaptor<ModuleEntity> cap = ArgumentCaptor.forClass(ModuleEntity.class);
  365 + verify(moduleMapper).update(cap.capture(), (Wrapper<ModuleEntity>) any());
  366 + ModuleEntity patch = cap.getValue();
  367 + assertThat(patch.getIIncrement()).isEqualTo(23);
  368 + assertThat(patch.getBDeleted()).isTrue();
  369 + assertThat(patch.getTDeletedDate()).isNotNull();
  370 + }
  371 +
  372 + @Test
  373 + void delete_softDeletedChildren_doesNotBlock() {
  374 + ModuleEntity t = existingTarget(24);
  375 + when(moduleMapper.selectById(24)).thenReturn(t);
  376 + // 子全部已软删除 → selectCount(bDeleted=0 过滤) 返回 0
  377 + when(moduleMapper.selectCount(any(Wrapper.class))).thenReturn(0L);
  378 + when(moduleMapper.update((ModuleEntity) any(), (Wrapper<ModuleEntity>) any())).thenReturn(1);
  379 +
  380 + ModuleDeleteResultVO vo = service.delete(24);
  381 + assertThat(vo.getBDeleted()).isTrue();
  382 + }
  383 +
  384 + @Test
  385 + void delete_concurrentRace_throws40421() {
  386 + ModuleEntity t = existingTarget(25);
  387 + when(moduleMapper.selectById(25)).thenReturn(t);
  388 + when(moduleMapper.selectCount(any(Wrapper.class))).thenReturn(0L);
  389 + // update 影响行数 0 → 视为并发删除
  390 + when(moduleMapper.update((ModuleEntity) any(), (Wrapper<ModuleEntity>) any())).thenReturn(0);
  391 +
  392 + assertThatThrownBy(() -> service.delete(25))
  393 + .isInstanceOf(BizException.class)
  394 + .extracting(e -> ((BizException) e).getCode())
  395 + .isEqualTo(ErrorCode.MOD_NOT_FOUND.getCode());
  396 + }
312 } 397 }