diff --git a/backend/src/test/java/com/xly/erp/modules/usr/UsrUserQueryIT.java b/backend/src/test/java/com/xly/erp/modules/usr/UsrUserQueryIT.java
new file mode 100644
index 0000000..9e2ed6c
--- /dev/null
+++ b/backend/src/test/java/com/xly/erp/modules/usr/UsrUserQueryIT.java
@@ -0,0 +1,296 @@
+package com.xly.erp.modules.usr;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+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.UsrEmployee;
+import com.xly.erp.modules.usr.entity.UsrUser;
+import com.xly.erp.modules.usr.mapper.UsrEmployeeMapper;
+import com.xly.erp.modules.usr.mapper.UsrUserMapper;
+import java.time.LocalDateTime;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+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;
+
+/**
+ * REQ-USR-003 T6:查询用户端到端验收回归(spec § 7 AC1-AC13)。
+ *
+ *
@SpringBootTest + 真实 MockMvc 安全链 + test profile 连测试库(Flyway 已 apply V1)。
+ * 认证态通过真实 JwtUtil 签发 token 走 JwtAuthenticationFilter 注入;查询无管理员限制
+ * (任意已认证用户可调用,spec § 8 D5)。每个用例自管理 fixture 清理(命名前缀
+ * {@code it3_user_} / {@code IT3职员} / {@code IT3_EMP_})。
+ */
+@SpringBootTest
+@AutoConfigureMockMvc
+@ActiveProfiles("test")
+class UsrUserQueryIT {
+
+ private static final String USER_PREFIX = "it3_user_";
+ private static final String EMP_NAME_PREFIX = "IT3职员";
+ private static final String EMP_NO_PREFIX = "IT3_EMP_";
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Autowired
+ private JwtUtil jwtUtil;
+
+ @Autowired
+ private UsrUserMapper usrUserMapper;
+
+ @Autowired
+ private UsrEmployeeMapper usrEmployeeMapper;
+
+ @BeforeEach
+ void cleanupBefore() {
+ purge();
+ }
+
+ @AfterEach
+ void cleanupAfter() {
+ purge();
+ }
+
+ private void purge() {
+ usrUserMapper.delete(Wrappers.lambdaQuery()
+ .likeRight(UsrUser::getSUserName, USER_PREFIX));
+ usrEmployeeMapper.delete(Wrappers.lambdaQuery()
+ .likeRight(UsrEmployee::getSEmployeeName, EMP_NAME_PREFIX));
+ }
+
+ private String token() {
+ return "Bearer " + jwtUtil.generateToken("it3_caller", "普通用户");
+ }
+
+ private Integer seedEmployee(String nameSuffix, String dept) {
+ UsrEmployee emp = new UsrEmployee();
+ emp.setSEmployeeName(EMP_NAME_PREFIX + nameSuffix);
+ emp.setSEmployeeNo(EMP_NO_PREFIX + nameSuffix);
+ emp.setSDepartment(dept);
+ usrEmployeeMapper.insert(emp);
+ return emp.getIIncrement();
+ }
+
+ private UsrUser seedUser(String nameSuffix, String creator, String userType) {
+ UsrUser u = new UsrUser();
+ u.setSUserName(USER_PREFIX + nameSuffix);
+ u.setSPassword("$2a$dummyhashvalue000000000000000000000");
+ u.setSUserType(userType);
+ u.setSLanguage("中文");
+ u.setICanModifyBill(0);
+ u.setIIsVoid(0);
+ u.setSCreator(creator);
+ usrUserMapper.insert(u);
+ return u;
+ }
+
+ @Test
+ void ac1EmptyConditionFullPage() throws Exception {
+ seedUser("a1x", "it3_creator", "普通用户");
+ seedUser("a1y", "it3_creator", "普通用户");
+
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token())
+ .param("pageNum", "1").param("pageSize", "10"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.pageNum").value(1))
+ .andExpect(jsonPath("$.data.pageSize").value(10))
+ .andExpect(jsonPath("$.data.records").isArray())
+ .andExpect(jsonPath("$.data.records[0].sUserName").exists())
+ .andExpect(jsonPath("$.data.records[0].sPassword").doesNotExist())
+ .andExpect(jsonPath("$.data.records[0].password").doesNotExist());
+ }
+
+ @Test
+ void ac2TextContains() throws Exception {
+ seedUser("contains_target", "it3_creator", "普通用户");
+ seedUser("other_one", "it3_creator", "普通用户");
+
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token())
+ .param("queryField", "用户名").param("matchType", "包含")
+ .param("queryValue", "contains_target"))
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "contains_target')]").exists())
+ .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "other_one')]").doesNotExist());
+ }
+
+ @Test
+ void ac3TextEquals() throws Exception {
+ seedUser("eq", "it3_creator", "普通用户");
+ seedUser("eq_suffix", "it3_creator", "普通用户");
+ String exact = USER_PREFIX + "eq";
+
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token())
+ .param("queryField", "用户名").param("matchType", "等于")
+ .param("queryValue", exact))
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.total").value(1))
+ .andExpect(jsonPath("$.data.records[0].sUserName").value(exact));
+ }
+
+ @Test
+ void ac4TextNotContains() throws Exception {
+ seedUser("nc1", "it3_creator_keep", "普通用户");
+ seedUser("nc2", "it3_creator_drop", "普通用户");
+
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token())
+ .param("queryField", "制单人").param("matchType", "不包含")
+ .param("queryValue", "drop").param("pageSize", "100"))
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "nc1')]").exists())
+ .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "nc2')]").doesNotExist());
+ }
+
+ @Test
+ void ac5EnumEquals() throws Exception {
+ seedUser("admin_u", "it3_creator", "超级管理员");
+ seedUser("normal_u", "it3_creator", "普通用户");
+
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token())
+ .param("queryField", "用户类型").param("matchType", "等于")
+ .param("queryValue", "超级管理员").param("pageSize", "100"))
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "admin_u')]").exists())
+ .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "normal_u')]").doesNotExist());
+ }
+
+ @Test
+ void ac6BoolFilter() throws Exception {
+ UsrUser voided = seedUser("voided", "it3_creator", "普通用户");
+ voided.setIIsVoid(1);
+ usrUserMapper.updateById(voided);
+ seedUser("active", "it3_creator", "普通用户");
+
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token())
+ .param("queryField", "作废").param("queryValue", "1").param("pageSize", "100"))
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "voided')]").exists())
+ .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "active')]").doesNotExist());
+
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token())
+ .param("queryField", "作废").param("queryValue", "0").param("pageSize", "100"))
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "active')]").exists())
+ .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "voided')]").doesNotExist());
+
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token())
+ .param("queryField", "作废").param("queryValue", "abc"))
+ .andExpect(jsonPath("$.code").value(40001));
+ }
+
+ @Test
+ void ac7DateFilter() throws Exception {
+ UsrUser dated = seedUser("dated", "it3_creator", "普通用户");
+ dated.setTLastLoginDate(LocalDateTime.of(2026, 6, 1, 12, 0, 0));
+ usrUserMapper.updateById(dated);
+
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token())
+ .param("queryField", "登录日期").param("matchType", "等于")
+ .param("queryValue", "2026-06-01").param("pageSize", "100"))
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "dated')]").exists());
+
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token())
+ .param("queryField", "登录日期").param("matchType", "等于")
+ .param("queryValue", "2026-13-99"))
+ .andExpect(jsonPath("$.code").value(40001));
+ }
+
+ @Test
+ void ac8CrossTableEmployeeDept() throws Exception {
+ Integer empId = seedEmployee("fin", "财务部IT3");
+ UsrUser linked = seedUser("linked", "it3_creator", "普通用户");
+ linked.setIEmployeeId(empId);
+ usrUserMapper.updateById(linked);
+ seedUser("unlinked", "it3_creator", "普通用户");
+
+ // 全量:关联用户带员工名 / 部门,未关联用户两列为 null。
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token()).param("pageSize", "100"))
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX
+ + "linked')].department").value(org.hamcrest.Matchers.hasItem("财务部IT3")))
+ .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX
+ + "linked')].employeeName").value(org.hamcrest.Matchers.hasItem(EMP_NAME_PREFIX + "fin")));
+
+ // 按部门跨表过滤:仅返回所属部门含「财务」的用户。
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token())
+ .param("queryField", "部门").param("matchType", "包含")
+ .param("queryValue", "财务").param("pageSize", "100"))
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "linked')]").exists())
+ .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "unlinked')]").doesNotExist());
+ }
+
+ @Test
+ void ac9NoMatchEmptyList() throws Exception {
+ seedUser("present", "it3_creator", "普通用户");
+
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token())
+ .param("queryField", "用户名").param("matchType", "等于")
+ .param("queryValue", "it3_user_zzz_no_such_user_xyz"))
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.total").value(0))
+ .andExpect(jsonPath("$.data.records").isEmpty());
+ }
+
+ @Test
+ void ac10DataOutOfRangeLastPage() throws Exception {
+ // 插 3 条同制单人 fixture,按制单人精确过滤 → total=3,pageSize=2 → 2 页;请求 page99 钳到末页 2。
+ seedUser("clamp1", "it3_clamp_creator", "普通用户");
+ seedUser("clamp2", "it3_clamp_creator", "普通用户");
+ seedUser("clamp3", "it3_clamp_creator", "普通用户");
+
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token())
+ .param("queryField", "制单人").param("matchType", "等于")
+ .param("queryValue", "it3_clamp_creator")
+ .param("pageNum", "99").param("pageSize", "2"))
+ .andExpect(jsonPath("$.code").value(0))
+ .andExpect(jsonPath("$.data.total").value(3))
+ .andExpect(jsonPath("$.data.pageNum").value(2))
+ .andExpect(jsonPath("$.data.records.length()").value(1));
+ }
+
+ @Test
+ void ac11PageParamInvalid() throws Exception {
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token()).param("pageNum", "0"))
+ .andExpect(jsonPath("$.code").value(42201));
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token()).param("pageSize", "0"))
+ .andExpect(jsonPath("$.code").value(42201));
+ mockMvc.perform(get("/api/usr/users").header("Authorization", token()).param("pageSize", "500"))
+ .andExpect(jsonPath("$.code").value(42201));
+ }
+
+ @Test
+ void ac12NoToken() throws Exception {
+ mockMvc.perform(get("/api/usr/users"))
+ .andExpect(status().isUnauthorized());
+ mockMvc.perform(get("/api/usr/users").header("Authorization", "Bearer invalid.token.value"))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ void ac13PasswordNeverLeaks() throws Exception {
+ seedUser("leakcheck", "it3_creator", "普通用户");
+ String body = mockMvc.perform(get("/api/usr/users").header("Authorization", token())
+ .param("pageSize", "100"))
+ .andExpect(jsonPath("$.code").value(0))
+ .andReturn().getResponse().getContentAsString();
+
+ assertThat(body).doesNotContain("sPassword");
+ assertThat(body.toLowerCase()).doesNotContain("password");
+ assertThat(body).doesNotContain("$2a$");
+ }
+}