Commit 3a64246d76e6df790fee4ac3353c65f5bf771de3
1 parent
3f1b9e89
feat(usr): PUT /api/users/{id} controller REQ-USR-002
Showing
2 changed files
with
193 additions
and
0 deletions
backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java
| @@ -2,11 +2,14 @@ package com.xly.erp.module.usr.controller; | @@ -2,11 +2,14 @@ package com.xly.erp.module.usr.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.usr.dto.UserCreateDTO; | 4 | import com.xly.erp.module.usr.dto.UserCreateDTO; |
| 5 | +import com.xly.erp.module.usr.dto.UserUpdateDTO; | ||
| 5 | import com.xly.erp.module.usr.service.UserService; | 6 | import com.xly.erp.module.usr.service.UserService; |
| 6 | import com.xly.erp.module.usr.vo.UserVO; | 7 | import com.xly.erp.module.usr.vo.UserVO; |
| 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; |
| @@ -23,4 +26,10 @@ public class UserController { | @@ -23,4 +26,10 @@ public class UserController { | ||
| 23 | public ApiResponse<UserVO> create(@Valid @RequestBody UserCreateDTO dto) { | 26 | public ApiResponse<UserVO> create(@Valid @RequestBody UserCreateDTO dto) { |
| 24 | return ApiResponse.ok(userService.create(dto)); | 27 | return ApiResponse.ok(userService.create(dto)); |
| 25 | } | 28 | } |
| 29 | + | ||
| 30 | + /** REQ-USR-002 用户修改 — REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('USR:UPDATE')") */ | ||
| 31 | + @PutMapping("/{id}") | ||
| 32 | + public ApiResponse<UserVO> update(@PathVariable Integer id, @Valid @RequestBody UserUpdateDTO dto) { | ||
| 33 | + return ApiResponse.ok(userService.update(id, dto)); | ||
| 34 | + } | ||
| 26 | } | 35 | } |
backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java
| @@ -2,6 +2,7 @@ package com.xly.erp.module.usr.controller; | @@ -2,6 +2,7 @@ package com.xly.erp.module.usr.controller; | ||
| 2 | 2 | ||
| 3 | import com.fasterxml.jackson.databind.ObjectMapper; | 3 | import com.fasterxml.jackson.databind.ObjectMapper; |
| 4 | import com.xly.erp.module.usr.dto.UserCreateDTO; | 4 | import com.xly.erp.module.usr.dto.UserCreateDTO; |
| 5 | +import com.xly.erp.module.usr.dto.UserUpdateDTO; | ||
| 5 | import com.xly.erp.module.usr.entity.PermissionCategoryEntity; | 6 | import com.xly.erp.module.usr.entity.PermissionCategoryEntity; |
| 6 | import com.xly.erp.module.usr.entity.StaffEntity; | 7 | import com.xly.erp.module.usr.entity.StaffEntity; |
| 7 | import com.xly.erp.module.usr.entity.UserEntity; | 8 | import com.xly.erp.module.usr.entity.UserEntity; |
| @@ -26,6 +27,7 @@ import java.util.List; | @@ -26,6 +27,7 @@ import java.util.List; | ||
| 26 | import static com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery; | 27 | import static com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery; |
| 27 | import static org.assertj.core.api.Assertions.assertThat; | 28 | import static org.assertj.core.api.Assertions.assertThat; |
| 28 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; | 29 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; |
| 30 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; | ||
| 29 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | 31 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; |
| 30 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | 32 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
| 31 | 33 | ||
| @@ -181,4 +183,186 @@ class UserControllerIT { | @@ -181,4 +183,186 @@ class UserControllerIT { | ||
| 181 | .andExpect(status().isOk()) | 183 | .andExpect(status().isOk()) |
| 182 | .andExpect(jsonPath("$.data.sPasswordHash").doesNotExist()); | 184 | .andExpect(jsonPath("$.data.sPasswordHash").doesNotExist()); |
| 183 | } | 185 | } |
| 186 | + | ||
| 187 | + // ============================================================ | ||
| 188 | + // REQ-USR-002 PUT 系列 | ||
| 189 | + // ============================================================ | ||
| 190 | + | ||
| 191 | + private Integer insertUser(String userName, Integer staffId, List<Integer> categoryIds) { | ||
| 192 | + UserEntity u = new UserEntity(); | ||
| 193 | + u.setSUserNo("uno_" + System.nanoTime()); | ||
| 194 | + u.setSUserName(userName); | ||
| 195 | + u.setIStaffId(staffId); | ||
| 196 | + u.setSUserType("普通用户"); | ||
| 197 | + u.setSLanguage("zh"); | ||
| 198 | + u.setBCanModifyDocs(false); | ||
| 199 | + u.setSPasswordHash("$2a$10$origUser"); | ||
| 200 | + u.setBDeleted(false); | ||
| 201 | + u.setTCreateDate(LocalDateTime.now()); | ||
| 202 | + userMapper.insert(u); | ||
| 203 | + for (Integer cid : categoryIds) { | ||
| 204 | + UserPermissionEntity up = new UserPermissionEntity(); | ||
| 205 | + up.setIUserId(u.getIIncrement()); | ||
| 206 | + up.setICategoryId(cid); | ||
| 207 | + up.setTCreateDate(LocalDateTime.now()); | ||
| 208 | + userPermissionMapper.insert(up); | ||
| 209 | + } | ||
| 210 | + return u.getIIncrement(); | ||
| 211 | + } | ||
| 212 | + | ||
| 213 | + private UserUpdateDTO updateDto(Integer staffId, List<Integer> permissionIds) { | ||
| 214 | + UserUpdateDTO d = new UserUpdateDTO(); | ||
| 215 | + d.setIStaffId(staffId); | ||
| 216 | + d.setSUserType("超级管理员"); | ||
| 217 | + d.setSLanguage("en"); | ||
| 218 | + d.setBCanModifyDocs(true); | ||
| 219 | + d.setPermissionCategoryIds(permissionIds); | ||
| 220 | + return d; | ||
| 221 | + } | ||
| 222 | + | ||
| 223 | + @Test | ||
| 224 | + void put_validUpdate_returns200_andDbReflects() throws Exception { | ||
| 225 | + Integer staff1 = insertStaff(); | ||
| 226 | + Integer staff2 = insertStaff(); | ||
| 227 | + Integer cat1 = insertCategory(); | ||
| 228 | + Integer cat2 = insertCategory(); | ||
| 229 | + Integer cat3 = insertCategory(); | ||
| 230 | + Integer userId = insertUser("upd_" + System.nanoTime(), staff1, List.of(cat1, cat2, cat3)); | ||
| 231 | + | ||
| 232 | + Integer catNew1 = insertCategory(); | ||
| 233 | + Integer catNew2 = insertCategory(); | ||
| 234 | + | ||
| 235 | + UserUpdateDTO dto = updateDto(staff2, List.of(catNew1, catNew2)); | ||
| 236 | + | ||
| 237 | + mockMvc.perform(put("/api/users/" + userId) | ||
| 238 | + .contentType(MediaType.APPLICATION_JSON) | ||
| 239 | + .content(json(dto))) | ||
| 240 | + .andExpect(status().isOk()) | ||
| 241 | + .andExpect(jsonPath("$.code").value(200)) | ||
| 242 | + .andExpect(jsonPath("$.data.iStaffId").value(staff2)) | ||
| 243 | + .andExpect(jsonPath("$.data.sUserType").value("超级管理员")) | ||
| 244 | + .andExpect(jsonPath("$.data.sLanguage").value("en")); | ||
| 245 | + | ||
| 246 | + UserEntity reloaded = userMapper.selectById(userId); | ||
| 247 | + assertThat(reloaded.getIStaffId()).isEqualTo(staff2); | ||
| 248 | + assertThat(reloaded.getSUserType()).isEqualTo("超级管理员"); | ||
| 249 | + assertThat(reloaded.getBCanModifyDocs()).isTrue(); | ||
| 250 | + Long upCount = userPermissionMapper.selectCount(lambdaQuery(UserPermissionEntity.class) | ||
| 251 | + .eq(UserPermissionEntity::getIUserId, userId)); | ||
| 252 | + assertThat(upCount).isEqualTo(2L); // 原 3 删 + 新 2 插 | ||
| 253 | + } | ||
| 254 | + | ||
| 255 | + @Test | ||
| 256 | + void put_clearStaffId_setsNull() throws Exception { | ||
| 257 | + Integer staffId = insertStaff(); | ||
| 258 | + Integer userId = insertUser("clr_" + System.nanoTime(), staffId, List.of()); | ||
| 259 | + | ||
| 260 | + UserUpdateDTO dto = updateDto(null, List.of()); | ||
| 261 | + | ||
| 262 | + mockMvc.perform(put("/api/users/" + userId) | ||
| 263 | + .contentType(MediaType.APPLICATION_JSON) | ||
| 264 | + .content(json(dto))) | ||
| 265 | + .andExpect(status().isOk()) | ||
| 266 | + .andExpect(jsonPath("$.code").value(200)); | ||
| 267 | + | ||
| 268 | + assertThat(userMapper.selectById(userId).getIStaffId()).isNull(); | ||
| 269 | + } | ||
| 270 | + | ||
| 271 | + @Test | ||
| 272 | + void put_emptyPermissionCategoryIds_clearsAssociations() throws Exception { | ||
| 273 | + Integer cat1 = insertCategory(); | ||
| 274 | + Integer cat2 = insertCategory(); | ||
| 275 | + Integer userId = insertUser("emp_" + System.nanoTime(), null, List.of(cat1, cat2)); | ||
| 276 | + | ||
| 277 | + UserUpdateDTO dto = updateDto(null, List.of()); | ||
| 278 | + | ||
| 279 | + mockMvc.perform(put("/api/users/" + userId) | ||
| 280 | + .contentType(MediaType.APPLICATION_JSON) | ||
| 281 | + .content(json(dto))) | ||
| 282 | + .andExpect(status().isOk()) | ||
| 283 | + .andExpect(jsonPath("$.code").value(200)); | ||
| 284 | + | ||
| 285 | + Long upCount = userPermissionMapper.selectCount(lambdaQuery(UserPermissionEntity.class) | ||
| 286 | + .eq(UserPermissionEntity::getIUserId, userId)); | ||
| 287 | + assertThat(upCount).isZero(); | ||
| 288 | + } | ||
| 289 | + | ||
| 290 | + @Test | ||
| 291 | + void put_targetNotFound_returns40431() throws Exception { | ||
| 292 | + UserUpdateDTO dto = updateDto(null, List.of()); | ||
| 293 | + mockMvc.perform(put("/api/users/999999") | ||
| 294 | + .contentType(MediaType.APPLICATION_JSON) | ||
| 295 | + .content(json(dto))) | ||
| 296 | + .andExpect(status().isOk()) | ||
| 297 | + .andExpect(jsonPath("$.code").value(40431)); | ||
| 298 | + } | ||
| 299 | + | ||
| 300 | + @Test | ||
| 301 | + void put_staffNotFound_returns40421() throws Exception { | ||
| 302 | + Integer userId = insertUser("nost_" + System.nanoTime(), null, List.of()); | ||
| 303 | + UserUpdateDTO dto = updateDto(999999, List.of()); | ||
| 304 | + mockMvc.perform(put("/api/users/" + userId) | ||
| 305 | + .contentType(MediaType.APPLICATION_JSON) | ||
| 306 | + .content(json(dto))) | ||
| 307 | + .andExpect(status().isOk()) | ||
| 308 | + .andExpect(jsonPath("$.code").value(40421)); | ||
| 309 | + } | ||
| 310 | + | ||
| 311 | + @Test | ||
| 312 | + void put_permissionCategoryNotFound_returns40422() throws Exception { | ||
| 313 | + Integer userId = insertUser("noc_" + System.nanoTime(), null, List.of()); | ||
| 314 | + UserUpdateDTO dto = updateDto(null, List.of(999999)); | ||
| 315 | + mockMvc.perform(put("/api/users/" + userId) | ||
| 316 | + .contentType(MediaType.APPLICATION_JSON) | ||
| 317 | + .content(json(dto))) | ||
| 318 | + .andExpect(status().isOk()) | ||
| 319 | + .andExpect(jsonPath("$.code").value(40422)); | ||
| 320 | + } | ||
| 321 | + | ||
| 322 | + @Test | ||
| 323 | + void put_missingRequired_returns40010() throws Exception { | ||
| 324 | + Integer userId = insertUser("miss_" + System.nanoTime(), null, List.of()); | ||
| 325 | + UserUpdateDTO dto = updateDto(null, List.of()); | ||
| 326 | + dto.setSUserType(null); // 必填缺失 | ||
| 327 | + mockMvc.perform(put("/api/users/" + userId) | ||
| 328 | + .contentType(MediaType.APPLICATION_JSON) | ||
| 329 | + .content(json(dto))) | ||
| 330 | + .andExpect(status().isOk()) | ||
| 331 | + .andExpect(jsonPath("$.code").value(40010)); | ||
| 332 | + } | ||
| 333 | + | ||
| 334 | + @Test | ||
| 335 | + void put_ignoresProtectedFields_doesNotChangeUserNoOrName() throws Exception { | ||
| 336 | + String origName = "prot_" + System.nanoTime(); | ||
| 337 | + Integer userId = insertUser(origName, null, List.of()); | ||
| 338 | + String origNo = userMapper.selectById(userId).getSUserNo(); | ||
| 339 | + String origHash = userMapper.selectById(userId).getSPasswordHash(); | ||
| 340 | + | ||
| 341 | + // 手工拼 body 含保护字段 | ||
| 342 | + String body = """ | ||
| 343 | + { | ||
| 344 | + "sUserNo": "hijack", | ||
| 345 | + "sUserName": "hijack", | ||
| 346 | + "sPasswordHash": "$2a$10$hijacked", | ||
| 347 | + "iStaffId": null, | ||
| 348 | + "sUserType": "超级管理员", | ||
| 349 | + "sLanguage": "zh-TW", | ||
| 350 | + "bCanModifyDocs": true, | ||
| 351 | + "permissionCategoryIds": [] | ||
| 352 | + } | ||
| 353 | + """; | ||
| 354 | + mockMvc.perform(put("/api/users/" + userId) | ||
| 355 | + .contentType(MediaType.APPLICATION_JSON) | ||
| 356 | + .content(body)) | ||
| 357 | + .andExpect(status().isOk()) | ||
| 358 | + .andExpect(jsonPath("$.code").value(200)); | ||
| 359 | + | ||
| 360 | + UserEntity reloaded = userMapper.selectById(userId); | ||
| 361 | + assertThat(reloaded.getSUserNo()).isEqualTo(origNo); | ||
| 362 | + assertThat(reloaded.getSUserName()).isEqualTo(origName); | ||
| 363 | + assertThat(reloaded.getSPasswordHash()).isEqualTo(origHash); | ||
| 364 | + // 但其他字段已修改 | ||
| 365 | + assertThat(reloaded.getSUserType()).isEqualTo("超级管理员"); | ||
| 366 | + assertThat(reloaded.getSLanguage()).isEqualTo("zh-TW"); | ||
| 367 | + } | ||
| 184 | } | 368 | } |