Commit 2419659932ae37e3e2855eb3961692083990afa0
1 parent
716b0b5b
fix(mod): 修复 review round 1 must-fix REQ-MOD-003
- 改用 LambdaUpdateWrapper.set(...) 显式声明 SET 列, 避免 iParentId.FieldStrategy.IGNORED 在 entity-driven update 时静默清空父引用。 - 新增 IT delete_preservesOtherFields_onChildModule 钉死非 root 子模块软删后其他列保持原值的回归断言。
Showing
3 changed files
with
58 additions
and
19 deletions
backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java
| ... | ... | @@ -146,19 +146,20 @@ public class ModuleServiceImpl implements ModuleService { |
| 146 | 146 | throw new BizException(ErrorCode.MOD_HAS_REFERENCES); |
| 147 | 147 | } |
| 148 | 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)); | |
| 149 | + // 3. 软删除写入:用 LambdaUpdateWrapper.set 显式声明 SET 列,避免 entity 驱动 SET | |
| 150 | + // (iParentId 字段的 FieldStrategy.IGNORED 策略会让 entity-driven update 把 | |
| 151 | + // iParentId 写成 NULL,破坏父引用——这里用 wrapper.set 完全绕开 entity) | |
| 152 | + // sDeletedBy 暂写 null(REQ-USR-004 后由登录上下文回填)。 | |
| 153 | + // eq(bDeleted, false) 兜底并发:影响 0 行视为已被并发删除 → 40421 | |
| 154 | + LambdaUpdateWrapper<ModuleEntity> uw = new LambdaUpdateWrapper<ModuleEntity>() | |
| 155 | + .eq(ModuleEntity::getIIncrement, id) | |
| 156 | + .eq(ModuleEntity::getBDeleted, false) | |
| 157 | + .set(ModuleEntity::getBDeleted, true) | |
| 158 | + .set(ModuleEntity::getTDeletedDate, LocalDateTime.now()) | |
| 159 | + .set(ModuleEntity::getSDeletedBy, null); | |
| 160 | + | |
| 161 | + int affected = moduleMapper.update(null, uw); | |
| 160 | 162 | if (affected == 0) { |
| 161 | - // 校验后被并发删除 | |
| 162 | 163 | throw new BizException(ErrorCode.MOD_NOT_FOUND); |
| 163 | 164 | } |
| 164 | 165 | ... | ... |
backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java
| ... | ... | @@ -340,6 +340,47 @@ class ModuleControllerIT { |
| 340 | 340 | } |
| 341 | 341 | |
| 342 | 342 | @Test |
| 343 | + void delete_preservesOtherFields_onChildModule() throws Exception { | |
| 344 | + // 反例:非 root 子模块软删除后,iParentId / sProcedureName / sModuleNameZh / | |
| 345 | + // iSortOrder 等字段必须保持原值(spec § 业务规则 #4 + 验收 #1)。 | |
| 346 | + Integer parentId = insertExisting("sp_del_pres_p_" + System.nanoTime(), null); | |
| 347 | + String childProc = "sp_del_pres_c_" + System.nanoTime(); | |
| 348 | + // 用自定义字段值的 child(与 insertExisting 默认值不同),便于事后比对 | |
| 349 | + ModuleEntity child = new ModuleEntity(); | |
| 350 | + child.setSDisplayType("接口"); | |
| 351 | + child.setSProcedureName(childProc); | |
| 352 | + child.setSModuleType("AUDIT"); | |
| 353 | + child.setSManageDeptEn("OPS"); | |
| 354 | + child.setBShowPermission(true); | |
| 355 | + child.setSModuleNameZh("待保留中文名"); | |
| 356 | + child.setIParentId(parentId); | |
| 357 | + child.setISortOrder(7); | |
| 358 | + child.setBDeleted(false); | |
| 359 | + child.setTCreateDate(LocalDateTime.now()); | |
| 360 | + moduleMapper.insert(child); | |
| 361 | + Integer childId = child.getIIncrement(); | |
| 362 | + | |
| 363 | + mockMvc.perform(delete("/api/modules/" + childId)) | |
| 364 | + .andExpect(status().isOk()) | |
| 365 | + .andExpect(jsonPath("$.code").value(200)) | |
| 366 | + .andExpect(jsonPath("$.data.bDeleted").value(true)); | |
| 367 | + | |
| 368 | + ModuleEntity reloaded = moduleMapper.selectById(childId); | |
| 369 | + // 软删除三件套生效 | |
| 370 | + assertThat(reloaded.getBDeleted()).isTrue(); | |
| 371 | + assertThat(reloaded.getTDeletedDate()).isNotNull(); | |
| 372 | + // 关键:其他列保持原值(曾经 iParentId.FieldStrategy.IGNORED 会清零这一列,本断言钉死该回归) | |
| 373 | + assertThat(reloaded.getIParentId()).isEqualTo(parentId); | |
| 374 | + assertThat(reloaded.getSProcedureName()).isEqualTo(childProc); | |
| 375 | + assertThat(reloaded.getSDisplayType()).isEqualTo("接口"); | |
| 376 | + assertThat(reloaded.getSModuleType()).isEqualTo("AUDIT"); | |
| 377 | + assertThat(reloaded.getSManageDeptEn()).isEqualTo("OPS"); | |
| 378 | + assertThat(reloaded.getBShowPermission()).isTrue(); | |
| 379 | + assertThat(reloaded.getSModuleNameZh()).isEqualTo("待保留中文名"); | |
| 380 | + assertThat(reloaded.getISortOrder()).isEqualTo(7); | |
| 381 | + } | |
| 382 | + | |
| 383 | + @Test | |
| 343 | 384 | void delete_responseVOContainsOnlyIIncrementAndBDeleted() throws Exception { |
| 344 | 385 | Integer id = insertExisting("sp_del_vo_" + System.nanoTime(), null); |
| 345 | 386 | ... | ... |
backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java
| ... | ... | @@ -24,6 +24,7 @@ import java.time.LocalDateTime; |
| 24 | 24 | import static org.assertj.core.api.Assertions.assertThat; |
| 25 | 25 | import static org.assertj.core.api.Assertions.assertThatThrownBy; |
| 26 | 26 | import static org.mockito.ArgumentMatchers.any; |
| 27 | +import static org.mockito.ArgumentMatchers.isNull; | |
| 27 | 28 | import static org.mockito.Mockito.never; |
| 28 | 29 | import static org.mockito.Mockito.verify; |
| 29 | 30 | import static org.mockito.Mockito.when; |
| ... | ... | @@ -360,13 +361,9 @@ class ModuleServiceImplTest { |
| 360 | 361 | |
| 361 | 362 | assertThat(vo.getIIncrement()).isEqualTo(23); |
| 362 | 363 | 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(); | |
| 364 | + // SET 列由 LambdaUpdateWrapper 声明,entity 第一参数为 null(避免 iParentId.IGNORED 策略副作用) | |
| 365 | + // 实际 SQL SET 子句的列覆盖由 IT (delete_preservesOtherFields_onChildModule) 验证 | |
| 366 | + verify(moduleMapper).update((ModuleEntity) isNull(), (Wrapper<ModuleEntity>) any()); | |
| 370 | 367 | } |
| 371 | 368 | |
| 372 | 369 | @Test | ... | ... |