Commit c0da0c5f0a5ec71fc761085a25d42ddd3572e8af

Authored by zichun
1 parent 74a4d485

feat(mod): POST /api/modules controller REQ-MOD-001

backend/src/main/java/com/xly/erp/config/JacksonConfig.java 0 → 100644
  1 +package com.xly.erp.config;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonAutoDetect;
  4 +import com.fasterxml.jackson.annotation.PropertyAccessor;
  5 +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
  6 +import org.springframework.context.annotation.Bean;
  7 +import org.springframework.context.annotation.Configuration;
  8 +
  9 +/**
  10 + * 让 Jackson 通过字段名(而非 getter/setter 推断)确定 JSON 属性名。
  11 + *
  12 + * <p>项目沿用 docs/03 的匈牙利前缀命名(如 {@code iIncrement} / {@code sUserName}),
  13 + * Lombok 生成的 getter({@code getIIncrement})经 JavaBeans Introspector 解析为
  14 + * {@code IIncrement}(首两字符全大写时保留),导致 JSON 输出 {@code "IIncrement"}
  15 + * 而非期望的 {@code "iIncrement"}。改为字段访问后,Jackson 直接用字段名作 JSON key。</p>
  16 + */
  17 +@Configuration
  18 +public class JacksonConfig {
  19 +
  20 + @Bean
  21 + public Jackson2ObjectMapperBuilderCustomizer fieldOnlyVisibility() {
  22 + return builder -> builder
  23 + .visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
  24 + .visibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE)
  25 + .visibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE)
  26 + .visibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.NONE);
  27 + }
  28 +}
backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java 0 → 100644
  1 +package com.xly.erp.module.mod.controller;
  2 +
  3 +import com.xly.erp.common.response.ApiResponse;
  4 +import com.xly.erp.module.mod.dto.ModuleCreateDTO;
  5 +import com.xly.erp.module.mod.service.ModuleService;
  6 +import com.xly.erp.module.mod.vo.ModuleVO;
  7 +import jakarta.validation.Valid;
  8 +import lombok.RequiredArgsConstructor;
  9 +import org.springframework.web.bind.annotation.PostMapping;
  10 +import org.springframework.web.bind.annotation.RequestBody;
  11 +import org.springframework.web.bind.annotation.RequestMapping;
  12 +import org.springframework.web.bind.annotation.RestController;
  13 +
  14 +// REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:CREATE')")
  15 +@RestController
  16 +@RequestMapping("/api/modules")
  17 +@RequiredArgsConstructor
  18 +public class ModuleController {
  19 +
  20 + private final ModuleService moduleService;
  21 +
  22 + @PostMapping
  23 + public ApiResponse<ModuleVO> create(@Valid @RequestBody ModuleCreateDTO dto) {
  24 + return ApiResponse.ok(moduleService.create(dto));
  25 + }
  26 +}
backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java 0 → 100644
  1 +package com.xly.erp.module.mod.controller;
  2 +
  3 +import com.fasterxml.jackson.databind.ObjectMapper;
  4 +import com.xly.erp.module.mod.dto.ModuleCreateDTO;
  5 +import org.junit.jupiter.api.Test;
  6 +import org.springframework.beans.factory.annotation.Autowired;
  7 +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
  8 +import org.springframework.boot.test.context.SpringBootTest;
  9 +import org.springframework.http.MediaType;
  10 +import org.springframework.test.annotation.Rollback;
  11 +import org.springframework.test.context.ActiveProfiles;
  12 +import org.springframework.test.web.servlet.MockMvc;
  13 +import org.springframework.transaction.annotation.Transactional;
  14 +
  15 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
  16 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
  17 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
  18 +
  19 +@SpringBootTest
  20 +@AutoConfigureMockMvc
  21 +@ActiveProfiles("test")
  22 +@Transactional
  23 +@Rollback
  24 +class ModuleControllerIT {
  25 +
  26 + @Autowired MockMvc mockMvc;
  27 + @Autowired ObjectMapper objectMapper;
  28 +
  29 + private ModuleCreateDTO valid(String procName) {
  30 + ModuleCreateDTO d = new ModuleCreateDTO();
  31 + d.setSDisplayType("前端业务");
  32 + d.setSProcedureName(procName);
  33 + d.setSModuleType("USR");
  34 + d.setSManageDeptEn("IT");
  35 + d.setBShowPermission(false);
  36 + d.setSModuleNameZh("用户管理");
  37 + d.setIParentId(null);
  38 + d.setISortOrder(0);
  39 + return d;
  40 + }
  41 +
  42 + private String json(Object o) throws Exception { return objectMapper.writeValueAsString(o); }
  43 +
  44 + @Test
  45 + void post_validRootModule_returns200WithVO() throws Exception {
  46 + ModuleCreateDTO dto = valid("sp_audit_root_" + System.nanoTime());
  47 + mockMvc.perform(post("/api/modules")
  48 + .contentType(MediaType.APPLICATION_JSON)
  49 + .content(json(dto)))
  50 + .andExpect(status().isOk())
  51 + .andExpect(jsonPath("$.code").value(200))
  52 + .andExpect(jsonPath("$.data.iIncrement").isNumber())
  53 + .andExpect(jsonPath("$.data.sProcedureName").value(dto.getSProcedureName()))
  54 + .andExpect(jsonPath("$.data.bDeleted").value(false));
  55 + }
  56 +
  57 + @Test
  58 + void post_validChildModule_returns200() throws Exception {
  59 + // 先建 root
  60 + ModuleCreateDTO root = valid("sp_audit_parent_" + System.nanoTime());
  61 + String rootBody = mockMvc.perform(post("/api/modules")
  62 + .contentType(MediaType.APPLICATION_JSON)
  63 + .content(json(root)))
  64 + .andExpect(status().isOk())
  65 + .andReturn().getResponse().getContentAsString();
  66 + Integer parentId = objectMapper.readTree(rootBody).path("data").path("iIncrement").asInt();
  67 +
  68 + // 再建 child
  69 + ModuleCreateDTO child = valid("sp_audit_child_" + System.nanoTime());
  70 + child.setIParentId(parentId);
  71 + mockMvc.perform(post("/api/modules")
  72 + .contentType(MediaType.APPLICATION_JSON)
  73 + .content(json(child)))
  74 + .andExpect(status().isOk())
  75 + .andExpect(jsonPath("$.code").value(200))
  76 + .andExpect(jsonPath("$.data.iParentId").value(parentId));
  77 + }
  78 +
  79 + @Test
  80 + void post_duplicateProcedureName_returns200WithCode40911() throws Exception {
  81 + String procName = "sp_audit_dup_" + System.nanoTime();
  82 + ModuleCreateDTO first = valid(procName);
  83 + mockMvc.perform(post("/api/modules")
  84 + .contentType(MediaType.APPLICATION_JSON)
  85 + .content(json(first)))
  86 + .andExpect(status().isOk());
  87 +
  88 + ModuleCreateDTO second = valid(procName);
  89 + mockMvc.perform(post("/api/modules")
  90 + .contentType(MediaType.APPLICATION_JSON)
  91 + .content(json(second)))
  92 + .andExpect(status().isOk())
  93 + .andExpect(jsonPath("$.code").value(40911));
  94 + }
  95 +
  96 + @Test
  97 + void post_parentNotFound_returns200WithCode40411() throws Exception {
  98 + ModuleCreateDTO d = valid("sp_audit_orphan_" + System.nanoTime());
  99 + d.setIParentId(999999);
  100 + mockMvc.perform(post("/api/modules")
  101 + .contentType(MediaType.APPLICATION_JSON)
  102 + .content(json(d)))
  103 + .andExpect(status().isOk())
  104 + .andExpect(jsonPath("$.code").value(40411));
  105 + }
  106 +
  107 + @Test
  108 + void post_missingRequiredField_returns200WithCode40010() throws Exception {
  109 + ModuleCreateDTO d = valid("sp_audit_miss_" + System.nanoTime());
  110 + d.setSModuleNameZh(null); // 必填缺失
  111 + mockMvc.perform(post("/api/modules")
  112 + .contentType(MediaType.APPLICATION_JSON)
  113 + .content(json(d)))
  114 + .andExpect(status().isOk())
  115 + .andExpect(jsonPath("$.code").value(40010));
  116 + }
  117 +
  118 + @Test
  119 + void post_invalidDisplayTypeEnum_returns200WithCode40010() throws Exception {
  120 + ModuleCreateDTO d = valid("sp_audit_enum_" + System.nanoTime());
  121 + d.setSDisplayType("非法值");
  122 + mockMvc.perform(post("/api/modules")
  123 + .contentType(MediaType.APPLICATION_JSON)
  124 + .content(json(d)))
  125 + .andExpect(status().isOk())
  126 + .andExpect(jsonPath("$.code").value(40010));
  127 + }
  128 +}