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 | 2 | |
| 3 | 3 | import com.xly.erp.module.mod.dto.ModuleCreateDTO; |
| 4 | 4 | import com.xly.erp.module.mod.dto.ModuleUpdateDTO; |
| 5 | +import com.xly.erp.module.mod.vo.ModuleDeleteResultVO; | |
| 5 | 6 | import com.xly.erp.module.mod.vo.ModuleVO; |
| 6 | 7 | |
| 7 | 8 | public interface ModuleService { |
| ... | ... | @@ -10,4 +11,7 @@ public interface ModuleService { |
| 10 | 11 | |
| 11 | 12 | /** REQ-MOD-002 模块修改 */ |
| 12 | 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 | 1 | package com.xly.erp.module.mod.service.impl; |
| 2 | 2 | |
| 3 | 3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| 4 | +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; | |
| 4 | 5 | import com.xly.erp.common.exception.BizException; |
| 5 | 6 | import com.xly.erp.common.response.ErrorCode; |
| 6 | 7 | import com.xly.erp.module.mod.dto.ModuleCreateDTO; |
| ... | ... | @@ -8,6 +9,7 @@ import com.xly.erp.module.mod.dto.ModuleUpdateDTO; |
| 8 | 9 | import com.xly.erp.module.mod.entity.ModuleEntity; |
| 9 | 10 | import com.xly.erp.module.mod.mapper.ModuleMapper; |
| 10 | 11 | import com.xly.erp.module.mod.service.ModuleService; |
| 12 | +import com.xly.erp.module.mod.vo.ModuleDeleteResultVO; | |
| 11 | 13 | import com.xly.erp.module.mod.vo.ModuleVO; |
| 12 | 14 | import lombok.RequiredArgsConstructor; |
| 13 | 15 | import org.springframework.dao.DuplicateKeyException; |
| ... | ... | @@ -124,4 +126,42 @@ public class ModuleServiceImpl implements ModuleService { |
| 124 | 126 | // 5. 返回 VO |
| 125 | 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 | 8 | import com.xly.erp.module.mod.entity.ModuleEntity; |
| 9 | 9 | import com.xly.erp.module.mod.mapper.ModuleMapper; |
| 10 | 10 | import com.xly.erp.module.mod.service.impl.ModuleServiceImpl; |
| 11 | +import com.xly.erp.module.mod.vo.ModuleDeleteResultVO; | |
| 11 | 12 | import com.xly.erp.module.mod.vo.ModuleVO; |
| 12 | 13 | import org.junit.jupiter.api.Test; |
| 13 | 14 | import org.junit.jupiter.api.extension.ExtendWith; |
| ... | ... | @@ -309,4 +310,88 @@ class ModuleServiceImplTest { |
| 309 | 310 | verify(moduleMapper).updateById(cap.capture()); |
| 310 | 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 | } | ... | ... |