From 329a341fb91548b18fcae838588d46694d49f208 Mon Sep 17 00:00:00 2001 From: zichun Date: Wed, 6 May 2026 17:47:11 +0800 Subject: [PATCH] feat(mod): PUT /api/modules/{id} controller REQ-MOD-002 --- backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java | 11 ++++++++++- backend/src/main/java/com/xly/erp/module/mod/entity/ModuleEntity.java | 4 +++- backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java | 167 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java b/backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java index 3252dc8..111a1ea 100644 --- a/backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java +++ b/backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java @@ -2,16 +2,18 @@ package com.xly.erp.module.mod.controller; import com.xly.erp.common.response.ApiResponse; import com.xly.erp.module.mod.dto.ModuleCreateDTO; +import com.xly.erp.module.mod.dto.ModuleUpdateDTO; import com.xly.erp.module.mod.service.ModuleService; import com.xly.erp.module.mod.vo.ModuleVO; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -// REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:CREATE')") @RestController @RequestMapping("/api/modules") @RequiredArgsConstructor @@ -19,8 +21,15 @@ public class ModuleController { private final ModuleService moduleService; + /** REQ-MOD-001 模块新增 — REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:CREATE')") */ @PostMapping public ApiResponse create(@Valid @RequestBody ModuleCreateDTO dto) { return ApiResponse.ok(moduleService.create(dto)); } + + /** REQ-MOD-002 模块修改 — REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:UPDATE')") */ + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Integer id, @Valid @RequestBody ModuleUpdateDTO dto) { + return ApiResponse.ok(moduleService.update(id, dto)); + } } diff --git a/backend/src/main/java/com/xly/erp/module/mod/entity/ModuleEntity.java b/backend/src/main/java/com/xly/erp/module/mod/entity/ModuleEntity.java index 4bb6c61..166e9c4 100644 --- a/backend/src/main/java/com/xly/erp/module/mod/entity/ModuleEntity.java +++ b/backend/src/main/java/com/xly/erp/module/mod/entity/ModuleEntity.java @@ -1,5 +1,6 @@ package com.xly.erp.module.mod.entity; +import com.baomidou.mybatisplus.annotation.FieldStrategy; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; @@ -51,7 +52,8 @@ public class ModuleEntity { @TableField("sModuleNameZh") private String sModuleNameZh; - @TableField("iParentId") + /** REQ-MOD-002 允许更新为 null(清空父模块),用 IGNORED 让 MP updateById 把 NULL 写入 SQL。 */ + @TableField(value = "iParentId", updateStrategy = FieldStrategy.IGNORED) private Integer iParentId; @TableField("iSortOrder") diff --git a/backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java b/backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java index 0669941..e9ed767 100644 --- a/backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java +++ b/backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java @@ -2,6 +2,9 @@ package com.xly.erp.module.mod.controller; import com.fasterxml.jackson.databind.ObjectMapper; import com.xly.erp.module.mod.dto.ModuleCreateDTO; +import com.xly.erp.module.mod.dto.ModuleUpdateDTO; +import com.xly.erp.module.mod.entity.ModuleEntity; +import com.xly.erp.module.mod.mapper.ModuleMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -12,7 +15,11 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -25,6 +32,7 @@ class ModuleControllerIT { @Autowired MockMvc mockMvc; @Autowired ObjectMapper objectMapper; + @Autowired ModuleMapper moduleMapper; private ModuleCreateDTO valid(String procName) { ModuleCreateDTO d = new ModuleCreateDTO(); @@ -125,4 +133,163 @@ class ModuleControllerIT { .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(40010)); } + + // ============================================================ + // REQ-MOD-002 PUT 系列 + // ============================================================ + + private Integer insertExisting(String procName, Integer parentId) { + ModuleEntity e = new ModuleEntity(); + e.setSDisplayType("前端业务"); + e.setSProcedureName(procName); + e.setSModuleType("USR"); + e.setSManageDeptEn("IT"); + e.setBShowPermission(false); + e.setSModuleNameZh("用户管理"); + e.setIParentId(parentId); + e.setISortOrder(0); + e.setBDeleted(false); + e.setTCreateDate(LocalDateTime.now()); + moduleMapper.insert(e); + return e.getIIncrement(); + } + + private ModuleUpdateDTO updateDto() { + ModuleUpdateDTO d = new ModuleUpdateDTO(); + d.setSDisplayType("系统配置"); + d.setSModuleType("USR_REVISED"); + d.setSManageDeptEn("OPS"); + d.setBShowPermission(true); + d.setSModuleNameZh("用户管理(修订)"); + d.setIParentId(null); + d.setISortOrder(5); + return d; + } + + @Test + void put_validUpdate_returns200() throws Exception { + String origProc = "sp_put_valid_" + System.nanoTime(); + Integer id = insertExisting(origProc, null); + mockMvc.perform(put("/api/modules/" + id) + .contentType(MediaType.APPLICATION_JSON) + .content(json(updateDto()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.iIncrement").value(id)) + .andExpect(jsonPath("$.data.sDisplayType").value("系统配置")) + .andExpect(jsonPath("$.data.sModuleNameZh").value("用户管理(修订)")) + .andExpect(jsonPath("$.data.sProcedureName").value(origProc)); + + ModuleEntity reloaded = moduleMapper.selectById(id); + assertThat(reloaded.getSModuleType()).isEqualTo("USR_REVISED"); + assertThat(reloaded.getSManageDeptEn()).isEqualTo("OPS"); + assertThat(reloaded.getBShowPermission()).isTrue(); + assertThat(reloaded.getISortOrder()).isEqualTo(5); + assertThat(reloaded.getSProcedureName()).isEqualTo(origProc); + } + + @Test + void put_setParentToNull_clearsParent() throws Exception { + Integer parentId = insertExisting("sp_put_parent_" + System.nanoTime(), null); + Integer childId = insertExisting("sp_put_child_" + System.nanoTime(), parentId); + + ModuleUpdateDTO d = updateDto(); + d.setIParentId(null); + + mockMvc.perform(put("/api/modules/" + childId) + .contentType(MediaType.APPLICATION_JSON) + .content(json(d))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.iParentId").doesNotExist()); + + assertThat(moduleMapper.selectById(childId).getIParentId()).isNull(); + } + + @Test + void put_targetNotFound_returns40421() throws Exception { + mockMvc.perform(put("/api/modules/999999") + .contentType(MediaType.APPLICATION_JSON) + .content(json(updateDto()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(40421)); + } + + @Test + void put_parentNotFound_returns40411() throws Exception { + Integer id = insertExisting("sp_put_orphan_" + System.nanoTime(), null); + ModuleUpdateDTO d = updateDto(); + d.setIParentId(999999); + mockMvc.perform(put("/api/modules/" + id) + .contentType(MediaType.APPLICATION_JSON) + .content(json(d))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(40411)); + } + + @Test + void put_parentSelfRef_returns40921() throws Exception { + Integer id = insertExisting("sp_put_self_" + System.nanoTime(), null); + ModuleUpdateDTO d = updateDto(); + d.setIParentId(id); + mockMvc.perform(put("/api/modules/" + id) + .contentType(MediaType.APPLICATION_JSON) + .content(json(d))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(40921)); + } + + @Test + void put_parentIsDescendant_returns40921() throws Exception { + // grandparent -> parent -> child;尝试把 grandparent 的 iParentId 设为 child + Integer grandId = insertExisting("sp_put_grand_" + System.nanoTime(), null); + Integer parentId = insertExisting("sp_put_par_" + System.nanoTime(), grandId); + Integer childId = insertExisting("sp_put_chi_" + System.nanoTime(), parentId); + + ModuleUpdateDTO d = updateDto(); + d.setIParentId(childId); + mockMvc.perform(put("/api/modules/" + grandId) + .contentType(MediaType.APPLICATION_JSON) + .content(json(d))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(40921)); + } + + @Test + void put_missingRequired_returns40010() throws Exception { + Integer id = insertExisting("sp_put_miss_" + System.nanoTime(), null); + ModuleUpdateDTO d = updateDto(); + d.setSModuleNameZh(null); + mockMvc.perform(put("/api/modules/" + id) + .contentType(MediaType.APPLICATION_JSON) + .content(json(d))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(40010)); + } + + @Test + void put_ignoresProcedureNameField_doesNotChange() throws Exception { + String origProc = "sp_put_keep_" + System.nanoTime(); + Integer id = insertExisting(origProc, null); + // 手工拼一个含 sProcedureName 的请求体(DTO 没声明该字段,Jackson 默认忽略) + String body = """ + { + "sDisplayType": "系统配置", + "sProcedureName": "hijack", + "sModuleType": "USR_REVISED", + "sManageDeptEn": "OPS", + "bShowPermission": true, + "sModuleNameZh": "用户管理(修订)", + "iSortOrder": 5 + } + """; + mockMvc.perform(put("/api/modules/" + id) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.sProcedureName").value(origProc)); + + assertThat(moduleMapper.selectById(id).getSProcedureName()).isEqualTo(origProc); + } } -- libgit2 0.22.2