Commit cba0d896d048b728c394e6298f877bb0e3a62c14

Authored by zichun
1 parent 0da97ed5

feat(mod): update module service REQ-MOD-002

backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java
1 1 package com.xly.erp.module.mod.service;
2 2  
3 3 import com.xly.erp.module.mod.dto.ModuleCreateDTO;
  4 +import com.xly.erp.module.mod.dto.ModuleUpdateDTO;
4 5 import com.xly.erp.module.mod.vo.ModuleVO;
5 6  
6 7 public interface ModuleService {
  8 + /** REQ-MOD-001 模块新增 */
7 9 ModuleVO create(ModuleCreateDTO dto);
  10 +
  11 + /** REQ-MOD-002 模块修改 */
  12 + ModuleVO update(Integer id, ModuleUpdateDTO dto);
8 13 }
... ...
backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java
... ... @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4 4 import com.xly.erp.common.exception.BizException;
5 5 import com.xly.erp.common.response.ErrorCode;
6 6 import com.xly.erp.module.mod.dto.ModuleCreateDTO;
  7 +import com.xly.erp.module.mod.dto.ModuleUpdateDTO;
7 8 import com.xly.erp.module.mod.entity.ModuleEntity;
8 9 import com.xly.erp.module.mod.mapper.ModuleMapper;
9 10 import com.xly.erp.module.mod.service.ModuleService;
... ... @@ -65,4 +66,62 @@ public class ModuleServiceImpl implements ModuleService {
65 66 // 5. 返回 VO
66 67 return ModuleVO.from(e);
67 68 }
  69 +
  70 + /** REQ-MOD-002 模块修改 */
  71 + @Override
  72 + @Transactional(rollbackFor = Exception.class)
  73 + public ModuleVO update(Integer id, ModuleUpdateDTO dto) {
  74 + // 1. 目标模块校验
  75 + ModuleEntity target = moduleMapper.selectById(id);
  76 + if (target == null || Boolean.TRUE.equals(target.getBDeleted())) {
  77 + throw new BizException(ErrorCode.MOD_NOT_FOUND);
  78 + }
  79 +
  80 + // 2. iParentId 校验
  81 + Integer newParentId = dto.getIParentId();
  82 + if (newParentId != null) {
  83 + // 2a. 自引用
  84 + if (newParentId.equals(id)) {
  85 + throw new BizException(ErrorCode.MOD_PARENT_LOOP);
  86 + }
  87 + // 2b. 父存在性
  88 + ModuleEntity parent = moduleMapper.selectById(newParentId);
  89 + if (parent == null || Boolean.TRUE.equals(parent.getBDeleted())) {
  90 + throw new BizException(ErrorCode.MOD_PARENT_NOT_FOUND);
  91 + }
  92 + // 2c. 环路检查:从 newParentId 沿父链 walk up,最多 5 层;遇到 id 即环路
  93 + ModuleEntity cur = parent;
  94 + int depth = 1;
  95 + while (cur != null && cur.getIParentId() != null && depth <= 5) {
  96 + if (cur.getIParentId().equals(id)) {
  97 + throw new BizException(ErrorCode.MOD_PARENT_LOOP);
  98 + }
  99 + ModuleEntity next = moduleMapper.selectById(cur.getIParentId());
  100 + if (next == null || Boolean.TRUE.equals(next.getBDeleted())) {
  101 + break;
  102 + }
  103 + cur = next;
  104 + depth++;
  105 + }
  106 + }
  107 +
  108 + // 3. 字段合并到 target(sProcedureName / iIncrement / tCreateDate / sCreatedBy / 多租户字段 / bDeleted 等保留原值)
  109 + target.setSDisplayType(dto.getSDisplayType());
  110 + target.setSModuleType(dto.getSModuleType());
  111 + target.setSManageDeptEn(dto.getSManageDeptEn());
  112 + target.setSModuleNameZh(dto.getSModuleNameZh());
  113 + if (dto.getBShowPermission() != null) {
  114 + target.setBShowPermission(dto.getBShowPermission());
  115 + }
  116 + target.setIParentId(dto.getIParentId()); // null 设根
  117 + if (dto.getISortOrder() != null) {
  118 + target.setISortOrder(dto.getISortOrder());
  119 + }
  120 +
  121 + // 4. 落库
  122 + moduleMapper.updateById(target);
  123 +
  124 + // 5. 返回 VO
  125 + return ModuleVO.from(target);
  126 + }
68 127 }
... ...
backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java
... ... @@ -4,18 +4,22 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
4 4 import com.xly.erp.common.exception.BizException;
5 5 import com.xly.erp.common.response.ErrorCode;
6 6 import com.xly.erp.module.mod.dto.ModuleCreateDTO;
  7 +import com.xly.erp.module.mod.dto.ModuleUpdateDTO;
7 8 import com.xly.erp.module.mod.entity.ModuleEntity;
8 9 import com.xly.erp.module.mod.mapper.ModuleMapper;
9 10 import com.xly.erp.module.mod.service.impl.ModuleServiceImpl;
10 11 import com.xly.erp.module.mod.vo.ModuleVO;
11 12 import org.junit.jupiter.api.Test;
12 13 import org.junit.jupiter.api.extension.ExtendWith;
  14 +import org.mockito.ArgumentCaptor;
13 15 import org.mockito.ArgumentMatchers;
14 16 import org.mockito.InjectMocks;
15 17 import org.mockito.Mock;
16 18 import org.mockito.junit.jupiter.MockitoExtension;
17 19 import org.springframework.dao.DuplicateKeyException;
18 20  
  21 +import java.time.LocalDateTime;
  22 +
19 23 import static org.assertj.core.api.Assertions.assertThat;
20 24 import static org.assertj.core.api.Assertions.assertThatThrownBy;
21 25 import static org.mockito.ArgumentMatchers.any;
... ... @@ -132,4 +136,177 @@ class ModuleServiceImplTest {
132 136 .extracting(e -> ((BizException) e).getCode())
133 137 .isEqualTo(ErrorCode.MOD_PROC_NAME_DUP.getCode());
134 138 }
  139 +
  140 + // ============================================================
  141 + // REQ-MOD-002 update 系列
  142 + // ============================================================
  143 +
  144 + private ModuleUpdateDTO updateDto() {
  145 + ModuleUpdateDTO d = new ModuleUpdateDTO();
  146 + d.setSDisplayType("系统配置");
  147 + d.setSModuleType("USR_REVISED");
  148 + d.setSManageDeptEn("OPS");
  149 + d.setBShowPermission(true);
  150 + d.setSModuleNameZh("用户管理(修订)");
  151 + d.setIParentId(null);
  152 + d.setISortOrder(5);
  153 + return d;
  154 + }
  155 +
  156 + private ModuleEntity existingTarget(int id) {
  157 + ModuleEntity t = new ModuleEntity();
  158 + t.setIIncrement(id);
  159 + t.setSDisplayType("前端业务");
  160 + t.setSProcedureName("sp_audit_existing");
  161 + t.setSModuleType("USR");
  162 + t.setSManageDeptEn("IT");
  163 + t.setBShowPermission(false);
  164 + t.setSModuleNameZh("用户管理");
  165 + t.setIParentId(null);
  166 + t.setISortOrder(0);
  167 + t.setBDeleted(false);
  168 + t.setTCreateDate(LocalDateTime.of(2026, 1, 1, 0, 0));
  169 + t.setSCreatedBy("admin");
  170 + return t;
  171 + }
  172 +
  173 + @Test
  174 + void update_targetNotFound_throws40421() {
  175 + when(moduleMapper.selectById(10)).thenReturn(null);
  176 +
  177 + assertThatThrownBy(() -> service.update(10, updateDto()))
  178 + .isInstanceOf(BizException.class)
  179 + .extracting(e -> ((BizException) e).getCode())
  180 + .isEqualTo(ErrorCode.MOD_NOT_FOUND.getCode());
  181 + }
  182 +
  183 + @Test
  184 + void update_targetSoftDeleted_throws40421() {
  185 + ModuleEntity t = existingTarget(11);
  186 + t.setBDeleted(true);
  187 + when(moduleMapper.selectById(11)).thenReturn(t);
  188 +
  189 + assertThatThrownBy(() -> service.update(11, updateDto()))
  190 + .isInstanceOf(BizException.class)
  191 + .extracting(e -> ((BizException) e).getCode())
  192 + .isEqualTo(ErrorCode.MOD_NOT_FOUND.getCode());
  193 + }
  194 +
  195 + @Test
  196 + void update_parentSelfReference_throws40921() {
  197 + ModuleEntity t = existingTarget(12);
  198 + when(moduleMapper.selectById(12)).thenReturn(t);
  199 + ModuleUpdateDTO d = updateDto();
  200 + d.setIParentId(12);
  201 +
  202 + assertThatThrownBy(() -> service.update(12, d))
  203 + .isInstanceOf(BizException.class)
  204 + .extracting(e -> ((BizException) e).getCode())
  205 + .isEqualTo(ErrorCode.MOD_PARENT_LOOP.getCode());
  206 + }
  207 +
  208 + @Test
  209 + void update_parentNotFound_throws40411() {
  210 + ModuleEntity t = existingTarget(13);
  211 + when(moduleMapper.selectById(13)).thenReturn(t);
  212 + when(moduleMapper.selectById(999999)).thenReturn(null);
  213 + ModuleUpdateDTO d = updateDto();
  214 + d.setIParentId(999999);
  215 +
  216 + assertThatThrownBy(() -> service.update(13, d))
  217 + .isInstanceOf(BizException.class)
  218 + .extracting(e -> ((BizException) e).getCode())
  219 + .isEqualTo(ErrorCode.MOD_PARENT_NOT_FOUND.getCode());
  220 + }
  221 +
  222 + @Test
  223 + void update_parentIsDescendant_throws40921() {
  224 + // 三层结构: 14(target) <- 20(child) <- 30(grand)
  225 + ModuleEntity target = existingTarget(14);
  226 + ModuleEntity child = existingTarget(20);
  227 + child.setIParentId(14);
  228 + ModuleEntity grand = existingTarget(30);
  229 + grand.setIParentId(20);
  230 +
  231 + when(moduleMapper.selectById(14)).thenReturn(target);
  232 + when(moduleMapper.selectById(30)).thenReturn(grand);
  233 + when(moduleMapper.selectById(20)).thenReturn(child);
  234 +
  235 + ModuleUpdateDTO d = updateDto();
  236 + d.setIParentId(30); // 想把 14 的父设成 30,但 30 是 14 的孙子 → 环路
  237 +
  238 + assertThatThrownBy(() -> service.update(14, d))
  239 + .isInstanceOf(BizException.class)
  240 + .extracting(e -> ((BizException) e).getCode())
  241 + .isEqualTo(ErrorCode.MOD_PARENT_LOOP.getCode());
  242 + }
  243 +
  244 + @Test
  245 + void update_full_returnsVOWithUpdatedFields() {
  246 + ModuleEntity target = existingTarget(15);
  247 + when(moduleMapper.selectById(15)).thenReturn(target);
  248 + when(moduleMapper.updateById((ModuleEntity) any())).thenReturn(1);
  249 +
  250 + ModuleVO vo = service.update(15, updateDto());
  251 +
  252 + ArgumentCaptor<ModuleEntity> cap = ArgumentCaptor.forClass(ModuleEntity.class);
  253 + verify(moduleMapper).updateById(cap.capture());
  254 + ModuleEntity saved = cap.getValue();
  255 +
  256 + // 已修改字段
  257 + assertThat(saved.getSDisplayType()).isEqualTo("系统配置");
  258 + assertThat(saved.getSModuleType()).isEqualTo("USR_REVISED");
  259 + assertThat(saved.getSManageDeptEn()).isEqualTo("OPS");
  260 + assertThat(saved.getSModuleNameZh()).isEqualTo("用户管理(修订)");
  261 + assertThat(saved.getBShowPermission()).isTrue();
  262 + assertThat(saved.getISortOrder()).isEqualTo(5);
  263 + assertThat(saved.getIParentId()).isNull();
  264 + // 保持原值
  265 + assertThat(saved.getIIncrement()).isEqualTo(15);
  266 + assertThat(saved.getSProcedureName()).isEqualTo("sp_audit_existing");
  267 + assertThat(saved.getTCreateDate()).isEqualTo(LocalDateTime.of(2026, 1, 1, 0, 0));
  268 + assertThat(saved.getSCreatedBy()).isEqualTo("admin");
  269 + assertThat(saved.getBDeleted()).isFalse();
  270 +
  271 + assertThat(vo.getSDisplayType()).isEqualTo("系统配置");
  272 + assertThat(vo.getSProcedureName()).isEqualTo("sp_audit_existing");
  273 + }
  274 +
  275 + @Test
  276 + void update_partialNullFields_keepsOriginalValues() {
  277 + ModuleEntity target = existingTarget(16);
  278 + target.setBShowPermission(true);
  279 + target.setISortOrder(99);
  280 + when(moduleMapper.selectById(16)).thenReturn(target);
  281 + when(moduleMapper.updateById((ModuleEntity) any())).thenReturn(1);
  282 +
  283 + ModuleUpdateDTO d = updateDto();
  284 + d.setBShowPermission(null);
  285 + d.setISortOrder(null);
  286 +
  287 + service.update(16, d);
  288 +
  289 + ArgumentCaptor<ModuleEntity> cap = ArgumentCaptor.forClass(ModuleEntity.class);
  290 + verify(moduleMapper).updateById(cap.capture());
  291 + ModuleEntity saved = cap.getValue();
  292 + assertThat(saved.getBShowPermission()).isTrue(); // 原值保留
  293 + assertThat(saved.getISortOrder()).isEqualTo(99); // 原值保留
  294 + }
  295 +
  296 + @Test
  297 + void update_clearParent_setsParentToNull() {
  298 + ModuleEntity target = existingTarget(17);
  299 + target.setIParentId(7); // 原本有父
  300 + when(moduleMapper.selectById(17)).thenReturn(target);
  301 + when(moduleMapper.updateById((ModuleEntity) any())).thenReturn(1);
  302 +
  303 + ModuleUpdateDTO d = updateDto();
  304 + d.setIParentId(null); // 显式清空
  305 +
  306 + service.update(17, d);
  307 +
  308 + ArgumentCaptor<ModuleEntity> cap = ArgumentCaptor.forClass(ModuleEntity.class);
  309 + verify(moduleMapper).updateById(cap.capture());
  310 + assertThat(cap.getValue().getIParentId()).isNull();
  311 + }
135 312 }
... ...