From 8e0ddfdc46bbf3ac0773d33e05df3ac25878f294 Mon Sep 17 00:00:00 2001 From: zichun Date: Wed, 6 May 2026 21:16:57 +0800 Subject: [PATCH] feat(usr): POST /api/users controller REQ-USR-001 --- backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java | 26 ++++++++++++++++++++++++++ backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java | 184 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+), 0 deletions(-) create mode 100644 backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java create mode 100644 backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java diff --git a/backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java b/backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java new file mode 100644 index 0000000..35ed264 --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java @@ -0,0 +1,26 @@ +package com.xly.erp.module.usr.controller; + +import com.xly.erp.common.response.ApiResponse; +import com.xly.erp.module.usr.dto.UserCreateDTO; +import com.xly.erp.module.usr.service.UserService; +import com.xly.erp.module.usr.vo.UserVO; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + /** REQ-USR-001 用户新增 — REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('USR:CREATE')") */ + @PostMapping + public ApiResponse create(@Valid @RequestBody UserCreateDTO dto) { + return ApiResponse.ok(userService.create(dto)); + } +} diff --git a/backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java b/backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java new file mode 100644 index 0000000..94558cf --- /dev/null +++ b/backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java @@ -0,0 +1,184 @@ +package com.xly.erp.module.usr.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.xly.erp.module.usr.dto.UserCreateDTO; +import com.xly.erp.module.usr.entity.PermissionCategoryEntity; +import com.xly.erp.module.usr.entity.StaffEntity; +import com.xly.erp.module.usr.entity.UserEntity; +import com.xly.erp.module.usr.entity.UserPermissionEntity; +import com.xly.erp.module.usr.mapper.PermissionCategoryMapper; +import com.xly.erp.module.usr.mapper.StaffMapper; +import com.xly.erp.module.usr.mapper.UserMapper; +import com.xly.erp.module.usr.mapper.UserPermissionMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +import static com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery; +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.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@Transactional +@Rollback +class UserControllerIT { + + @Autowired MockMvc mockMvc; + @Autowired ObjectMapper objectMapper; + @Autowired UserMapper userMapper; + @Autowired StaffMapper staffMapper; + @Autowired PermissionCategoryMapper permissionCategoryMapper; + @Autowired UserPermissionMapper userPermissionMapper; + + private UserCreateDTO baseDto(String userName) { + UserCreateDTO d = new UserCreateDTO(); + d.setSUserNo("uno_" + System.nanoTime()); + d.setSUserName(userName); + d.setSUserType("普通用户"); + d.setSLanguage("zh"); + return d; + } + + private String json(Object o) throws Exception { return objectMapper.writeValueAsString(o); } + + private Integer insertStaff() { + StaffEntity s = new StaffEntity(); + s.setSStaffNo("st_" + System.nanoTime()); + s.setSStaffName("员工A"); + s.setBDeleted(false); + s.setTCreateDate(LocalDateTime.now()); + staffMapper.insert(s); + return s.getIIncrement(); + } + + private Integer insertCategory() { + PermissionCategoryEntity p = new PermissionCategoryEntity(); + p.setSCategoryCode("c_" + System.nanoTime()); + p.setSCategoryName("分类"); + p.setISortOrder(0); + p.setBDeleted(false); + p.setTCreateDate(LocalDateTime.now()); + permissionCategoryMapper.insert(p); + return p.getIIncrement(); + } + + @Test + void post_minimalFields_returns200() throws Exception { + UserCreateDTO dto = baseDto("alice_" + System.nanoTime()); + + mockMvc.perform(post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json(dto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.iIncrement").isNumber()) + .andExpect(jsonPath("$.data.sUserName").value(dto.getSUserName())) + .andExpect(jsonPath("$.data.bCanModifyDocs").value(false)) + .andExpect(jsonPath("$.data.permissionCategoryIds").isArray()) + .andExpect(jsonPath("$.data.permissionCategoryIds.length()").value(0)); + } + + @Test + void post_withStaffAndPermissions_returns200_andDbAssociated() throws Exception { + Integer staffId = insertStaff(); + Integer cat1 = insertCategory(); + Integer cat2 = insertCategory(); + Integer cat3 = insertCategory(); + + UserCreateDTO dto = baseDto("bob_" + System.nanoTime()); + dto.setIStaffId(staffId); + dto.setPermissionCategoryIds(List.of(cat1, cat2, cat3)); + + mockMvc.perform(post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json(dto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.iStaffId").value(staffId)); + + UserEntity u = userMapper.selectOne(lambdaQuery(UserEntity.class) + .eq(UserEntity::getSUserName, dto.getSUserName())); + assertThat(u).isNotNull(); + Long upCount = userPermissionMapper.selectCount(lambdaQuery(UserPermissionEntity.class) + .eq(UserPermissionEntity::getIUserId, u.getIIncrement())); + assertThat(upCount).isEqualTo(3L); + } + + @Test + void post_duplicateUserName_returns40921() throws Exception { + String userName = "dup_" + System.nanoTime(); + UserCreateDTO first = baseDto(userName); + mockMvc.perform(post("/api/users").contentType(MediaType.APPLICATION_JSON).content(json(first))) + .andExpect(status().isOk()); + + UserCreateDTO second = baseDto(userName); + mockMvc.perform(post("/api/users").contentType(MediaType.APPLICATION_JSON).content(json(second))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(40921)); + } + + @Test + void post_staffNotFound_returns40421() throws Exception { + UserCreateDTO dto = baseDto("noStaff_" + System.nanoTime()); + dto.setIStaffId(999999); + mockMvc.perform(post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json(dto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(40421)); + } + + @Test + void post_permissionCategoryNotFound_returns40422() throws Exception { + UserCreateDTO dto = baseDto("noCat_" + System.nanoTime()); + dto.setPermissionCategoryIds(List.of(999999)); + mockMvc.perform(post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json(dto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(40422)); + } + + @Test + void post_passwordHashedInDb_notPlaintext() throws Exception { + UserCreateDTO dto = baseDto("pw_" + System.nanoTime()); + + mockMvc.perform(post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json(dto))) + .andExpect(status().isOk()); + + UserEntity u = userMapper.selectOne(lambdaQuery(UserEntity.class) + .eq(UserEntity::getSUserName, dto.getSUserName())); + assertThat(u.getSPasswordHash()) + .satisfiesAnyOf( + h -> assertThat(h).startsWith("$2a$"), + h -> assertThat(h).startsWith("$2b$"), + h -> assertThat(h).startsWith("$2y$")) + .doesNotContain("666666"); + } + + @Test + void post_responseExcludesSPasswordHash() throws Exception { + UserCreateDTO dto = baseDto("priv_" + System.nanoTime()); + + mockMvc.perform(post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json(dto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.sPasswordHash").doesNotExist()); + } +} -- libgit2 0.22.2