Commit 329a341fb91548b18fcae838588d46694d49f208

Authored by zichun
1 parent cba0d896

feat(mod): PUT /api/modules/{id} controller REQ-MOD-002

backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java
@@ -2,16 +2,18 @@ package com.xly.erp.module.mod.controller; @@ -2,16 +2,18 @@ package com.xly.erp.module.mod.controller;
2 2
3 import com.xly.erp.common.response.ApiResponse; 3 import com.xly.erp.common.response.ApiResponse;
4 import com.xly.erp.module.mod.dto.ModuleCreateDTO; 4 import com.xly.erp.module.mod.dto.ModuleCreateDTO;
  5 +import com.xly.erp.module.mod.dto.ModuleUpdateDTO;
5 import com.xly.erp.module.mod.service.ModuleService; 6 import com.xly.erp.module.mod.service.ModuleService;
6 import com.xly.erp.module.mod.vo.ModuleVO; 7 import com.xly.erp.module.mod.vo.ModuleVO;
7 import jakarta.validation.Valid; 8 import jakarta.validation.Valid;
8 import lombok.RequiredArgsConstructor; 9 import lombok.RequiredArgsConstructor;
  10 +import org.springframework.web.bind.annotation.PathVariable;
9 import org.springframework.web.bind.annotation.PostMapping; 11 import org.springframework.web.bind.annotation.PostMapping;
  12 +import org.springframework.web.bind.annotation.PutMapping;
10 import org.springframework.web.bind.annotation.RequestBody; 13 import org.springframework.web.bind.annotation.RequestBody;
11 import org.springframework.web.bind.annotation.RequestMapping; 14 import org.springframework.web.bind.annotation.RequestMapping;
12 import org.springframework.web.bind.annotation.RestController; 15 import org.springframework.web.bind.annotation.RestController;
13 16
14 -// REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:CREATE')")  
15 @RestController 17 @RestController
16 @RequestMapping("/api/modules") 18 @RequestMapping("/api/modules")
17 @RequiredArgsConstructor 19 @RequiredArgsConstructor
@@ -19,8 +21,15 @@ public class ModuleController { @@ -19,8 +21,15 @@ public class ModuleController {
19 21
20 private final ModuleService moduleService; 22 private final ModuleService moduleService;
21 23
  24 + /** REQ-MOD-001 模块新增 — REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:CREATE')") */
22 @PostMapping 25 @PostMapping
23 public ApiResponse<ModuleVO> create(@Valid @RequestBody ModuleCreateDTO dto) { 26 public ApiResponse<ModuleVO> create(@Valid @RequestBody ModuleCreateDTO dto) {
24 return ApiResponse.ok(moduleService.create(dto)); 27 return ApiResponse.ok(moduleService.create(dto));
25 } 28 }
  29 +
  30 + /** REQ-MOD-002 模块修改 — REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:UPDATE')") */
  31 + @PutMapping("/{id}")
  32 + public ApiResponse<ModuleVO> update(@PathVariable Integer id, @Valid @RequestBody ModuleUpdateDTO dto) {
  33 + return ApiResponse.ok(moduleService.update(id, dto));
  34 + }
26 } 35 }
backend/src/main/java/com/xly/erp/module/mod/entity/ModuleEntity.java
1 package com.xly.erp.module.mod.entity; 1 package com.xly.erp.module.mod.entity;
2 2
  3 +import com.baomidou.mybatisplus.annotation.FieldStrategy;
3 import com.baomidou.mybatisplus.annotation.IdType; 4 import com.baomidou.mybatisplus.annotation.IdType;
4 import com.baomidou.mybatisplus.annotation.TableField; 5 import com.baomidou.mybatisplus.annotation.TableField;
5 import com.baomidou.mybatisplus.annotation.TableId; 6 import com.baomidou.mybatisplus.annotation.TableId;
@@ -51,7 +52,8 @@ public class ModuleEntity { @@ -51,7 +52,8 @@ public class ModuleEntity {
51 @TableField("sModuleNameZh") 52 @TableField("sModuleNameZh")
52 private String sModuleNameZh; 53 private String sModuleNameZh;
53 54
54 - @TableField("iParentId") 55 + /** REQ-MOD-002 允许更新为 null(清空父模块),用 IGNORED 让 MP updateById 把 NULL 写入 SQL。 */
  56 + @TableField(value = "iParentId", updateStrategy = FieldStrategy.IGNORED)
55 private Integer iParentId; 57 private Integer iParentId;
56 58
57 @TableField("iSortOrder") 59 @TableField("iSortOrder")
backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java
@@ -2,6 +2,9 @@ package com.xly.erp.module.mod.controller; @@ -2,6 +2,9 @@ package com.xly.erp.module.mod.controller;
2 2
3 import com.fasterxml.jackson.databind.ObjectMapper; 3 import com.fasterxml.jackson.databind.ObjectMapper;
4 import com.xly.erp.module.mod.dto.ModuleCreateDTO; 4 import com.xly.erp.module.mod.dto.ModuleCreateDTO;
  5 +import com.xly.erp.module.mod.dto.ModuleUpdateDTO;
  6 +import com.xly.erp.module.mod.entity.ModuleEntity;
  7 +import com.xly.erp.module.mod.mapper.ModuleMapper;
5 import org.junit.jupiter.api.Test; 8 import org.junit.jupiter.api.Test;
6 import org.springframework.beans.factory.annotation.Autowired; 9 import org.springframework.beans.factory.annotation.Autowired;
7 import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 10 import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
@@ -12,7 +15,11 @@ import org.springframework.test.context.ActiveProfiles; @@ -12,7 +15,11 @@ import org.springframework.test.context.ActiveProfiles;
12 import org.springframework.test.web.servlet.MockMvc; 15 import org.springframework.test.web.servlet.MockMvc;
13 import org.springframework.transaction.annotation.Transactional; 16 import org.springframework.transaction.annotation.Transactional;
14 17
  18 +import java.time.LocalDateTime;
  19 +
  20 +import static org.assertj.core.api.Assertions.assertThat;
15 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 21 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
  22 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
16 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 23 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
17 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 24 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
18 25
@@ -25,6 +32,7 @@ class ModuleControllerIT { @@ -25,6 +32,7 @@ class ModuleControllerIT {
25 32
26 @Autowired MockMvc mockMvc; 33 @Autowired MockMvc mockMvc;
27 @Autowired ObjectMapper objectMapper; 34 @Autowired ObjectMapper objectMapper;
  35 + @Autowired ModuleMapper moduleMapper;
28 36
29 private ModuleCreateDTO valid(String procName) { 37 private ModuleCreateDTO valid(String procName) {
30 ModuleCreateDTO d = new ModuleCreateDTO(); 38 ModuleCreateDTO d = new ModuleCreateDTO();
@@ -125,4 +133,163 @@ class ModuleControllerIT { @@ -125,4 +133,163 @@ class ModuleControllerIT {
125 .andExpect(status().isOk()) 133 .andExpect(status().isOk())
126 .andExpect(jsonPath("$.code").value(40010)); 134 .andExpect(jsonPath("$.code").value(40010));
127 } 135 }
  136 +
  137 + // ============================================================
  138 + // REQ-MOD-002 PUT 系列
  139 + // ============================================================
  140 +
  141 + private Integer insertExisting(String procName, Integer parentId) {
  142 + ModuleEntity e = new ModuleEntity();
  143 + e.setSDisplayType("前端业务");
  144 + e.setSProcedureName(procName);
  145 + e.setSModuleType("USR");
  146 + e.setSManageDeptEn("IT");
  147 + e.setBShowPermission(false);
  148 + e.setSModuleNameZh("用户管理");
  149 + e.setIParentId(parentId);
  150 + e.setISortOrder(0);
  151 + e.setBDeleted(false);
  152 + e.setTCreateDate(LocalDateTime.now());
  153 + moduleMapper.insert(e);
  154 + return e.getIIncrement();
  155 + }
  156 +
  157 + private ModuleUpdateDTO updateDto() {
  158 + ModuleUpdateDTO d = new ModuleUpdateDTO();
  159 + d.setSDisplayType("系统配置");
  160 + d.setSModuleType("USR_REVISED");
  161 + d.setSManageDeptEn("OPS");
  162 + d.setBShowPermission(true);
  163 + d.setSModuleNameZh("用户管理(修订)");
  164 + d.setIParentId(null);
  165 + d.setISortOrder(5);
  166 + return d;
  167 + }
  168 +
  169 + @Test
  170 + void put_validUpdate_returns200() throws Exception {
  171 + String origProc = "sp_put_valid_" + System.nanoTime();
  172 + Integer id = insertExisting(origProc, null);
  173 + mockMvc.perform(put("/api/modules/" + id)
  174 + .contentType(MediaType.APPLICATION_JSON)
  175 + .content(json(updateDto())))
  176 + .andExpect(status().isOk())
  177 + .andExpect(jsonPath("$.code").value(200))
  178 + .andExpect(jsonPath("$.data.iIncrement").value(id))
  179 + .andExpect(jsonPath("$.data.sDisplayType").value("系统配置"))
  180 + .andExpect(jsonPath("$.data.sModuleNameZh").value("用户管理(修订)"))
  181 + .andExpect(jsonPath("$.data.sProcedureName").value(origProc));
  182 +
  183 + ModuleEntity reloaded = moduleMapper.selectById(id);
  184 + assertThat(reloaded.getSModuleType()).isEqualTo("USR_REVISED");
  185 + assertThat(reloaded.getSManageDeptEn()).isEqualTo("OPS");
  186 + assertThat(reloaded.getBShowPermission()).isTrue();
  187 + assertThat(reloaded.getISortOrder()).isEqualTo(5);
  188 + assertThat(reloaded.getSProcedureName()).isEqualTo(origProc);
  189 + }
  190 +
  191 + @Test
  192 + void put_setParentToNull_clearsParent() throws Exception {
  193 + Integer parentId = insertExisting("sp_put_parent_" + System.nanoTime(), null);
  194 + Integer childId = insertExisting("sp_put_child_" + System.nanoTime(), parentId);
  195 +
  196 + ModuleUpdateDTO d = updateDto();
  197 + d.setIParentId(null);
  198 +
  199 + mockMvc.perform(put("/api/modules/" + childId)
  200 + .contentType(MediaType.APPLICATION_JSON)
  201 + .content(json(d)))
  202 + .andExpect(status().isOk())
  203 + .andExpect(jsonPath("$.code").value(200))
  204 + .andExpect(jsonPath("$.data.iParentId").doesNotExist());
  205 +
  206 + assertThat(moduleMapper.selectById(childId).getIParentId()).isNull();
  207 + }
  208 +
  209 + @Test
  210 + void put_targetNotFound_returns40421() throws Exception {
  211 + mockMvc.perform(put("/api/modules/999999")
  212 + .contentType(MediaType.APPLICATION_JSON)
  213 + .content(json(updateDto())))
  214 + .andExpect(status().isOk())
  215 + .andExpect(jsonPath("$.code").value(40421));
  216 + }
  217 +
  218 + @Test
  219 + void put_parentNotFound_returns40411() throws Exception {
  220 + Integer id = insertExisting("sp_put_orphan_" + System.nanoTime(), null);
  221 + ModuleUpdateDTO d = updateDto();
  222 + d.setIParentId(999999);
  223 + mockMvc.perform(put("/api/modules/" + id)
  224 + .contentType(MediaType.APPLICATION_JSON)
  225 + .content(json(d)))
  226 + .andExpect(status().isOk())
  227 + .andExpect(jsonPath("$.code").value(40411));
  228 + }
  229 +
  230 + @Test
  231 + void put_parentSelfRef_returns40921() throws Exception {
  232 + Integer id = insertExisting("sp_put_self_" + System.nanoTime(), null);
  233 + ModuleUpdateDTO d = updateDto();
  234 + d.setIParentId(id);
  235 + mockMvc.perform(put("/api/modules/" + id)
  236 + .contentType(MediaType.APPLICATION_JSON)
  237 + .content(json(d)))
  238 + .andExpect(status().isOk())
  239 + .andExpect(jsonPath("$.code").value(40921));
  240 + }
  241 +
  242 + @Test
  243 + void put_parentIsDescendant_returns40921() throws Exception {
  244 + // grandparent -> parent -> child;尝试把 grandparent 的 iParentId 设为 child
  245 + Integer grandId = insertExisting("sp_put_grand_" + System.nanoTime(), null);
  246 + Integer parentId = insertExisting("sp_put_par_" + System.nanoTime(), grandId);
  247 + Integer childId = insertExisting("sp_put_chi_" + System.nanoTime(), parentId);
  248 +
  249 + ModuleUpdateDTO d = updateDto();
  250 + d.setIParentId(childId);
  251 + mockMvc.perform(put("/api/modules/" + grandId)
  252 + .contentType(MediaType.APPLICATION_JSON)
  253 + .content(json(d)))
  254 + .andExpect(status().isOk())
  255 + .andExpect(jsonPath("$.code").value(40921));
  256 + }
  257 +
  258 + @Test
  259 + void put_missingRequired_returns40010() throws Exception {
  260 + Integer id = insertExisting("sp_put_miss_" + System.nanoTime(), null);
  261 + ModuleUpdateDTO d = updateDto();
  262 + d.setSModuleNameZh(null);
  263 + mockMvc.perform(put("/api/modules/" + id)
  264 + .contentType(MediaType.APPLICATION_JSON)
  265 + .content(json(d)))
  266 + .andExpect(status().isOk())
  267 + .andExpect(jsonPath("$.code").value(40010));
  268 + }
  269 +
  270 + @Test
  271 + void put_ignoresProcedureNameField_doesNotChange() throws Exception {
  272 + String origProc = "sp_put_keep_" + System.nanoTime();
  273 + Integer id = insertExisting(origProc, null);
  274 + // 手工拼一个含 sProcedureName 的请求体(DTO 没声明该字段,Jackson 默认忽略)
  275 + String body = """
  276 + {
  277 + "sDisplayType": "系统配置",
  278 + "sProcedureName": "hijack",
  279 + "sModuleType": "USR_REVISED",
  280 + "sManageDeptEn": "OPS",
  281 + "bShowPermission": true,
  282 + "sModuleNameZh": "用户管理(修订)",
  283 + "iSortOrder": 5
  284 + }
  285 + """;
  286 + mockMvc.perform(put("/api/modules/" + id)
  287 + .contentType(MediaType.APPLICATION_JSON)
  288 + .content(body))
  289 + .andExpect(status().isOk())
  290 + .andExpect(jsonPath("$.code").value(200))
  291 + .andExpect(jsonPath("$.data.sProcedureName").value(origProc));
  292 +
  293 + assertThat(moduleMapper.selectById(id).getSProcedureName()).isEqualTo(origProc);
  294 + }
128 } 295 }