Commit fa2044fdb90576e3abbb8d79cc3576df88bdd06e
1 parent
5379f491
feat(mod): delete module service REQ-MOD-003
Showing
3 changed files
with
129 additions
and
0 deletions
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 | } |