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 index 35ed264..0cf230e 100644 --- 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 @@ -2,11 +2,14 @@ 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.dto.UserUpdateDTO; 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.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; @@ -23,4 +26,10 @@ public class UserController { public ApiResponse create(@Valid @RequestBody UserCreateDTO dto) { return ApiResponse.ok(userService.create(dto)); } + + /** REQ-USR-002 用户修改 — REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('USR:UPDATE')") */ + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Integer id, @Valid @RequestBody UserUpdateDTO dto) { + return ApiResponse.ok(userService.update(id, 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 index 94558cf..273afdb 100644 --- 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 @@ -2,6 +2,7 @@ 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.dto.UserUpdateDTO; import com.xly.erp.module.usr.entity.PermissionCategoryEntity; import com.xly.erp.module.usr.entity.StaffEntity; import com.xly.erp.module.usr.entity.UserEntity; @@ -26,6 +27,7 @@ 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.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -181,4 +183,186 @@ class UserControllerIT { .andExpect(status().isOk()) .andExpect(jsonPath("$.data.sPasswordHash").doesNotExist()); } + + // ============================================================ + // REQ-USR-002 PUT 系列 + // ============================================================ + + private Integer insertUser(String userName, Integer staffId, List categoryIds) { + UserEntity u = new UserEntity(); + u.setSUserNo("uno_" + System.nanoTime()); + u.setSUserName(userName); + u.setIStaffId(staffId); + u.setSUserType("普通用户"); + u.setSLanguage("zh"); + u.setBCanModifyDocs(false); + u.setSPasswordHash("$2a$10$origUser"); + u.setBDeleted(false); + u.setTCreateDate(LocalDateTime.now()); + userMapper.insert(u); + for (Integer cid : categoryIds) { + UserPermissionEntity up = new UserPermissionEntity(); + up.setIUserId(u.getIIncrement()); + up.setICategoryId(cid); + up.setTCreateDate(LocalDateTime.now()); + userPermissionMapper.insert(up); + } + return u.getIIncrement(); + } + + private UserUpdateDTO updateDto(Integer staffId, List permissionIds) { + UserUpdateDTO d = new UserUpdateDTO(); + d.setIStaffId(staffId); + d.setSUserType("超级管理员"); + d.setSLanguage("en"); + d.setBCanModifyDocs(true); + d.setPermissionCategoryIds(permissionIds); + return d; + } + + @Test + void put_validUpdate_returns200_andDbReflects() throws Exception { + Integer staff1 = insertStaff(); + Integer staff2 = insertStaff(); + Integer cat1 = insertCategory(); + Integer cat2 = insertCategory(); + Integer cat3 = insertCategory(); + Integer userId = insertUser("upd_" + System.nanoTime(), staff1, List.of(cat1, cat2, cat3)); + + Integer catNew1 = insertCategory(); + Integer catNew2 = insertCategory(); + + UserUpdateDTO dto = updateDto(staff2, List.of(catNew1, catNew2)); + + mockMvc.perform(put("/api/users/" + userId) + .contentType(MediaType.APPLICATION_JSON) + .content(json(dto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.iStaffId").value(staff2)) + .andExpect(jsonPath("$.data.sUserType").value("超级管理员")) + .andExpect(jsonPath("$.data.sLanguage").value("en")); + + UserEntity reloaded = userMapper.selectById(userId); + assertThat(reloaded.getIStaffId()).isEqualTo(staff2); + assertThat(reloaded.getSUserType()).isEqualTo("超级管理员"); + assertThat(reloaded.getBCanModifyDocs()).isTrue(); + Long upCount = userPermissionMapper.selectCount(lambdaQuery(UserPermissionEntity.class) + .eq(UserPermissionEntity::getIUserId, userId)); + assertThat(upCount).isEqualTo(2L); // 原 3 删 + 新 2 插 + } + + @Test + void put_clearStaffId_setsNull() throws Exception { + Integer staffId = insertStaff(); + Integer userId = insertUser("clr_" + System.nanoTime(), staffId, List.of()); + + UserUpdateDTO dto = updateDto(null, List.of()); + + mockMvc.perform(put("/api/users/" + userId) + .contentType(MediaType.APPLICATION_JSON) + .content(json(dto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)); + + assertThat(userMapper.selectById(userId).getIStaffId()).isNull(); + } + + @Test + void put_emptyPermissionCategoryIds_clearsAssociations() throws Exception { + Integer cat1 = insertCategory(); + Integer cat2 = insertCategory(); + Integer userId = insertUser("emp_" + System.nanoTime(), null, List.of(cat1, cat2)); + + UserUpdateDTO dto = updateDto(null, List.of()); + + mockMvc.perform(put("/api/users/" + userId) + .contentType(MediaType.APPLICATION_JSON) + .content(json(dto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)); + + Long upCount = userPermissionMapper.selectCount(lambdaQuery(UserPermissionEntity.class) + .eq(UserPermissionEntity::getIUserId, userId)); + assertThat(upCount).isZero(); + } + + @Test + void put_targetNotFound_returns40431() throws Exception { + UserUpdateDTO dto = updateDto(null, List.of()); + mockMvc.perform(put("/api/users/999999") + .contentType(MediaType.APPLICATION_JSON) + .content(json(dto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(40431)); + } + + @Test + void put_staffNotFound_returns40421() throws Exception { + Integer userId = insertUser("nost_" + System.nanoTime(), null, List.of()); + UserUpdateDTO dto = updateDto(999999, List.of()); + mockMvc.perform(put("/api/users/" + userId) + .contentType(MediaType.APPLICATION_JSON) + .content(json(dto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(40421)); + } + + @Test + void put_permissionCategoryNotFound_returns40422() throws Exception { + Integer userId = insertUser("noc_" + System.nanoTime(), null, List.of()); + UserUpdateDTO dto = updateDto(null, List.of(999999)); + mockMvc.perform(put("/api/users/" + userId) + .contentType(MediaType.APPLICATION_JSON) + .content(json(dto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(40422)); + } + + @Test + void put_missingRequired_returns40010() throws Exception { + Integer userId = insertUser("miss_" + System.nanoTime(), null, List.of()); + UserUpdateDTO dto = updateDto(null, List.of()); + dto.setSUserType(null); // 必填缺失 + mockMvc.perform(put("/api/users/" + userId) + .contentType(MediaType.APPLICATION_JSON) + .content(json(dto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(40010)); + } + + @Test + void put_ignoresProtectedFields_doesNotChangeUserNoOrName() throws Exception { + String origName = "prot_" + System.nanoTime(); + Integer userId = insertUser(origName, null, List.of()); + String origNo = userMapper.selectById(userId).getSUserNo(); + String origHash = userMapper.selectById(userId).getSPasswordHash(); + + // 手工拼 body 含保护字段 + String body = """ + { + "sUserNo": "hijack", + "sUserName": "hijack", + "sPasswordHash": "$2a$10$hijacked", + "iStaffId": null, + "sUserType": "超级管理员", + "sLanguage": "zh-TW", + "bCanModifyDocs": true, + "permissionCategoryIds": [] + } + """; + mockMvc.perform(put("/api/users/" + userId) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)); + + UserEntity reloaded = userMapper.selectById(userId); + assertThat(reloaded.getSUserNo()).isEqualTo(origNo); + assertThat(reloaded.getSUserName()).isEqualTo(origName); + assertThat(reloaded.getSPasswordHash()).isEqualTo(origHash); + // 但其他字段已修改 + assertThat(reloaded.getSUserType()).isEqualTo("超级管理员"); + assertThat(reloaded.getSLanguage()).isEqualTo("zh-TW"); + } }