Commit 2419659932ae37e3e2855eb3961692083990afa0

Authored by zichun
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 子模块软删后其他列保持原值的回归断言。
backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java
@@ -146,19 +146,20 @@ public class ModuleServiceImpl implements ModuleService { @@ -146,19 +146,20 @@ public class ModuleServiceImpl implements ModuleService {
146 throw new BizException(ErrorCode.MOD_HAS_REFERENCES); 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 if (affected == 0) { 162 if (affected == 0) {
161 - // 校验后被并发删除  
162 throw new BizException(ErrorCode.MOD_NOT_FOUND); 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,6 +340,47 @@ class ModuleControllerIT {
340 } 340 }
341 341
342 @Test 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 void delete_responseVOContainsOnlyIIncrementAndBDeleted() throws Exception { 384 void delete_responseVOContainsOnlyIIncrementAndBDeleted() throws Exception {
344 Integer id = insertExisting("sp_del_vo_" + System.nanoTime(), null); 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,6 +24,7 @@ import java.time.LocalDateTime;
24 import static org.assertj.core.api.Assertions.assertThat; 24 import static org.assertj.core.api.Assertions.assertThat;
25 import static org.assertj.core.api.Assertions.assertThatThrownBy; 25 import static org.assertj.core.api.Assertions.assertThatThrownBy;
26 import static org.mockito.ArgumentMatchers.any; 26 import static org.mockito.ArgumentMatchers.any;
  27 +import static org.mockito.ArgumentMatchers.isNull;
27 import static org.mockito.Mockito.never; 28 import static org.mockito.Mockito.never;
28 import static org.mockito.Mockito.verify; 29 import static org.mockito.Mockito.verify;
29 import static org.mockito.Mockito.when; 30 import static org.mockito.Mockito.when;
@@ -360,13 +361,9 @@ class ModuleServiceImplTest { @@ -360,13 +361,9 @@ class ModuleServiceImplTest {
360 361
361 assertThat(vo.getIIncrement()).isEqualTo(23); 362 assertThat(vo.getIIncrement()).isEqualTo(23);
362 assertThat(vo.getBDeleted()).isTrue(); 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 @Test 369 @Test