diff --git a/backend/src/test/java/com/xly/erp/modules/usr/UsrUserUpdateIT.java b/backend/src/test/java/com/xly/erp/modules/usr/UsrUserUpdateIT.java
new file mode 100644
index 0000000..3c3ee1a
--- /dev/null
+++ b/backend/src/test/java/com/xly/erp/modules/usr/UsrUserUpdateIT.java
@@ -0,0 +1,304 @@
+package com.xly.erp.modules.usr;
+
+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;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.xly.erp.common.security.JwtUtil;
+import com.xly.erp.modules.usr.entity.UsrPermission;
+import com.xly.erp.modules.usr.entity.UsrUser;
+import com.xly.erp.modules.usr.entity.UsrUserPermission;
+import com.xly.erp.modules.usr.mapper.UsrPermissionMapper;
+import com.xly.erp.modules.usr.mapper.UsrUserMapper;
+import com.xly.erp.modules.usr.mapper.UsrUserPermissionMapper;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.AfterEach;
+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.test.context.ActiveProfiles;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+/**
+ * REQ-USR-002 T5:修改用户端到端验收回归(spec § 7)。
+ *
+ *
@SpringBootTest + 真实 MockMvc 安全链 + test profile 连测试库(Flyway 已 apply V1)。
+ * 认证态通过真实 JwtUtil 签发 token 走 JwtAuthenticationFilter 注入;真实库读写做端到端确认。
+ * 每个用例自管理 fixture 清理(命名前缀 {@code it2_user_} / {@code IT2_PERM_})。
+ *
+ * 边界说明(spec § 7):AC4「禁用实时生效」、AC5「角色变更实时生效」依赖 REQ-USR-004 登录 /
+ * REQ-USR-003 查询接口,尚未实现;本 IT 以「PUT iIsVoid=1 / 改 sUserType 后 selectById
+ * 读回库内 iIsVoid==1 / sUserType 为新值」做后端落库层等价验证,登录 / 查询联动留待对应 REQ
+ * 的 IT 覆盖。
+ */
+@SpringBootTest
+@AutoConfigureMockMvc
+@ActiveProfiles("test")
+class UsrUserUpdateIT {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Autowired
+ private JwtUtil jwtUtil;
+
+ @Autowired
+ private UsrUserMapper usrUserMapper;
+
+ @Autowired
+ private UsrUserPermissionMapper usrUserPermissionMapper;
+
+ @Autowired
+ private UsrPermissionMapper usrPermissionMapper;
+
+ private static final String CALLER = "it2_admin";
+
+ @AfterEach
+ void cleanup() {
+ usrUserMapper.delete(Wrappers.lambdaQuery()
+ .likeRight(UsrUser::getSUserName, "it2_user_"));
+ usrPermissionMapper.delete(Wrappers.lambdaQuery()
+ .likeRight(UsrPermission::getSPermissionCode, "IT2_PERM_"));
+ }
+
+ private String adminToken() {
+ return "Bearer " + jwtUtil.generateToken(CALLER, "超级管理员");
+ }
+
+ private String normalToken() {
+ return "Bearer " + jwtUtil.generateToken("it2_normal", "普通用户");
+ }
+
+ private Map createBody(String userName) {
+ Map body = new HashMap<>();
+ body.put("sUserName", userName);
+ body.put("sLanguage", "中文");
+ return body;
+ }
+
+ private int createUser(String userName) throws Exception {
+ MvcResult result = mockMvc.perform(post("/api/usr/users")
+ .header("Authorization", adminToken())
+ .contentType("application/json")
+ .content(objectMapper.writeValueAsString(createBody(userName))))
+ .andExpect(jsonPath("$.code").value(0))
+ .andReturn();
+ Map, ?> resp = objectMapper.readValue(result.getResponse().getContentAsString(), Map.class);
+ return (Integer) ((Map, ?>) resp.get("data")).get("id");
+ }
+
+ private int insertPermissionFixture(String codeSuffix) {
+ UsrPermission perm = new UsrPermission();
+ perm.setSPermissionName("IT2权限" + codeSuffix);
+ perm.setSPermissionCode("IT2_PERM_" + codeSuffix);
+ usrPermissionMapper.insert(perm);
+ return perm.getIIncrement();
+ }
+
+ // ---------------- AC1:基本信息修改落库,且身份 / 密码 / 审计列不变 ----------------
+
+ @Test
+ void ac1UpdateBasicInfoPersists() throws Exception {
+ int id = createUser("it2_user_ac1");
+ UsrUser before = usrUserMapper.selectById(id);
+
+ Map body = new HashMap<>();
+ body.put("sUserType", "超级管理员");
+ body.put("sLanguage", "英文");
+ body.put("iCanModifyBill", 1);
+ body.put("sUserNo", "NO-AC1");
+
+ mockMvc.perform(put("/api/usr/users/" + id)
+ .header("Authorization", adminToken())
+ .contentType("application/json")
+ .content(objectMapper.writeValueAsString(body)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.id").value(id));
+
+ UsrUser after = usrUserMapper.selectById(id);
+ assertThat(after.getSUserType()).isEqualTo("超级管理员");
+ assertThat(after.getSLanguage()).isEqualTo("英文");
+ assertThat(after.getICanModifyBill()).isEqualTo(1);
+ assertThat(after.getSUserNo()).isEqualTo("NO-AC1");
+ // 身份 / 密码 / 审计列字节级不变。
+ assertThat(after.getSUserName()).isEqualTo(before.getSUserName());
+ assertThat(after.getSPassword()).isEqualTo(before.getSPassword());
+ assertThat(after.getSCreator()).isEqualTo(before.getSCreator());
+ assertThat(after.getTCreateDate()).isEqualTo(before.getTCreateDate());
+ }
+
+ // ---------------- AC2:目标用户不存在 → 40401,无写入 ----------------
+
+ @Test
+ void ac2UpdateNonExistentReturns40401() throws Exception {
+ Map body = new HashMap<>();
+ body.put("sUserType", "普通用户");
+ body.put("sLanguage", "中文");
+
+ mockMvc.perform(put("/api/usr/users/2000000001")
+ .header("Authorization", adminToken())
+ .contentType("application/json")
+ .content(objectMapper.writeValueAsString(body)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(40401));
+
+ assertThat(usrUserMapper.selectById(2000000001)).isNull();
+ }
+
+ // ---------------- AC3:非法参数(不存在职员)→ 40001,整体回滚无副作用 ----------------
+
+ @Test
+ void ac3InvalidParamRollsBack() throws Exception {
+ int id = createUser("it2_user_ac3");
+ UsrUser before = usrUserMapper.selectById(id);
+
+ Map body = new HashMap<>();
+ body.put("sUserType", "超级管理员");
+ body.put("sLanguage", "英文");
+ body.put("iEmployeeId", 2000000002); // 不存在的职员
+
+ mockMvc.perform(put("/api/usr/users/" + id)
+ .header("Authorization", adminToken())
+ .contentType("application/json")
+ .content(objectMapper.writeValueAsString(body)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(40001));
+
+ UsrUser after = usrUserMapper.selectById(id);
+ // 目标用户行各列与调用前一致(无副作用)。
+ assertThat(after.getSUserType()).isEqualTo(before.getSUserType());
+ assertThat(after.getSLanguage()).isEqualTo(before.getSLanguage());
+ assertThat(after.getIEmployeeId()).isEqualTo(before.getIEmployeeId());
+ }
+
+ // ---------------- AC6:权限组全量覆盖 / 清空 / 不改 ----------------
+
+ @Test
+ void ac6PermissionOverwrite() throws Exception {
+ int id = createUser("it2_user_ac6");
+ int permA = insertPermissionFixture("A");
+ int permB = insertPermissionFixture("B");
+ int permC = insertPermissionFixture("C");
+ // 预置该用户授权为 {a, c}
+ usrUserPermissionMapper.insert(new UsrUserPermission(id, permA));
+ usrUserPermissionMapper.insert(new UsrUserPermission(id, permC));
+
+ Map body = new HashMap<>();
+ body.put("sUserType", "普通用户");
+ body.put("sLanguage", "中文");
+ body.put("permissionIds", List.of(permA, permB, permA)); // 去重后 {a, b}
+
+ mockMvc.perform(put("/api/usr/users/" + id)
+ .header("Authorization", adminToken())
+ .contentType("application/json")
+ .content(objectMapper.writeValueAsString(body)))
+ .andExpect(jsonPath("$.code").value(0));
+
+ List grants = usrUserPermissionMapper.selectList(
+ Wrappers.lambdaQuery().eq(UsrUserPermission::getIUserId, id));
+ assertThat(grants).extracting(UsrUserPermission::getIPermissionId)
+ .containsExactlyInAnyOrder(permA, permB); // c 被删、b 新增、a 去重一次
+
+ // 传 [] → 清空全部授权
+ Map clearBody = new HashMap<>();
+ clearBody.put("sUserType", "普通用户");
+ clearBody.put("sLanguage", "中文");
+ clearBody.put("permissionIds", List.of());
+ mockMvc.perform(put("/api/usr/users/" + id)
+ .header("Authorization", adminToken())
+ .contentType("application/json")
+ .content(objectMapper.writeValueAsString(clearBody)))
+ .andExpect(jsonPath("$.code").value(0));
+ long afterClear = usrUserPermissionMapper.selectCount(
+ Wrappers.lambdaQuery().eq(UsrUserPermission::getIUserId, id));
+ assertThat(afterClear).isZero();
+
+ // 预置一条授权后,不传 permissionIds → 授权不变
+ usrUserPermissionMapper.insert(new UsrUserPermission(id, permA));
+ Map noPermBody = new HashMap<>();
+ noPermBody.put("sUserType", "普通用户");
+ noPermBody.put("sLanguage", "中文");
+ mockMvc.perform(put("/api/usr/users/" + id)
+ .header("Authorization", adminToken())
+ .contentType("application/json")
+ .content(objectMapper.writeValueAsString(noPermBody)))
+ .andExpect(jsonPath("$.code").value(0));
+ long afterNoChange = usrUserPermissionMapper.selectCount(
+ Wrappers.lambdaQuery().eq(UsrUserPermission::getIUserId, id));
+ assertThat(afterNoChange).isEqualTo(1L);
+ }
+
+ // ---------------- AC7:非管理员 / 无 token 被拦截,目标行未被修改 ----------------
+
+ @Test
+ void ac7NonAdminAndNoTokenBlocked() throws Exception {
+ int id = createUser("it2_user_ac7");
+ UsrUser before = usrUserMapper.selectById(id);
+
+ Map body = new HashMap<>();
+ body.put("sUserType", "超级管理员");
+ body.put("sLanguage", "英文");
+
+ // 普通用户 token → 40301
+ mockMvc.perform(put("/api/usr/users/" + id)
+ .header("Authorization", normalToken())
+ .contentType("application/json")
+ .content(objectMapper.writeValueAsString(body)))
+ .andExpect(jsonPath("$.code").value(40301));
+
+ // 无 token → 401(安全链拦截)
+ mockMvc.perform(put("/api/usr/users/" + id)
+ .contentType("application/json")
+ .content(objectMapper.writeValueAsString(body)))
+ .andExpect(status().isUnauthorized());
+
+ UsrUser after = usrUserMapper.selectById(id);
+ assertThat(after.getSUserType()).isEqualTo(before.getSUserType());
+ assertThat(after.getSLanguage()).isEqualTo(before.getSLanguage());
+ }
+
+ // ---------------- AC8:密码不变且不出现在响应;iIsVoid 落库(AC4/AC5 落库层等价验证) ----------------
+
+ @Test
+ void ac8PasswordUnchangedAndAbsentFromResponse() throws Exception {
+ int id = createUser("it2_user_ac8");
+ UsrUser before = usrUserMapper.selectById(id);
+
+ Map body = new HashMap<>();
+ body.put("sUserType", "超级管理员"); // AC5 角色变更落库层等价验证
+ body.put("sLanguage", "中文");
+ body.put("iIsVoid", 1); // AC4 禁用落库层等价验证
+
+ MvcResult result = mockMvc.perform(put("/api/usr/users/" + id)
+ .header("Authorization", adminToken())
+ .contentType("application/json")
+ .content(objectMapper.writeValueAsString(body)))
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.id").value(id))
+ .andExpect(jsonPath("$.data.sPassword").doesNotExist())
+ .andExpect(jsonPath("$.data.password").doesNotExist())
+ .andReturn();
+
+ String responseBody = result.getResponse().getContentAsString();
+ assertThat(responseBody.toLowerCase()).doesNotContain("password");
+
+ UsrUser after = usrUserMapper.selectById(id);
+ // 密码列字节级不变。
+ assertThat(after.getSPassword()).isEqualTo(before.getSPassword());
+ // AC4 / AC5 落库层等价验证:禁用状态 / 角色变更读回库内为新值。
+ assertThat(after.getIIsVoid()).isEqualTo(1);
+ assertThat(after.getSUserType()).isEqualTo("超级管理员");
+ }
+}