develop: 开发员工管理模块(端到端验证)

This commit is contained in:
Pipeline Agent 2026-08-15 01:20:44 +08:00
parent 0b5e437650
commit 661eb156b7

View File

@ -1,22 +1,20 @@
package com.hr.staff; package com.hr.staff;
import com.fasterxml.jackson.databind.JsonNode; import com.hr.staff.repository.StaffEmployeeRepository;
import com.fasterxml.jackson.databind.ObjectMapper; import com.hr.staff.security.AuthUser;
import com.hr.staff.security.JwtService; import com.hr.staff.security.JwtService;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@ -29,231 +27,203 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
*/ */
@SpringBootTest @SpringBootTest
@AutoConfigureMockMvc @AutoConfigureMockMvc
@ActiveProfiles("dev")
@TestMethodOrder(MethodOrderer.MethodName.class)
class StaffApiIntegrationTest { class StaffApiIntegrationTest {
private static final AtomicInteger SEQ = new AtomicInteger(1); @Autowired
private static final AtomicInteger ID_SEQ = new AtomicInteger(1); private MockMvc mockMvc;
@Autowired @Autowired
MockMvc mockMvc; private JwtService jwtService;
@Autowired @Autowired
ObjectMapper objectMapper; private StaffEmployeeRepository employeeRepository;
@Autowired private String adminToken() {
JwtService jwtService; return "Bearer " + jwtService.generateToken(new AuthUser(1L, "admin", Set.of("ADMIN"), 1003L));
private String token(String role, Long deptId) {
return jwtService.generate(9000L + SEQ.get(), role.toLowerCase() + "_user", Set.of(role), deptId);
} }
private String uniqueNo() { private String hrToken() {
return "EMP" + String.format("%08d", SEQ.incrementAndGet()); return "Bearer " + jwtService.generateToken(new AuthUser(2L, "hr", Set.of("HR"), 1003L));
} }
/** private String employeeToken(Long departmentId) {
* 生成符合身份证号格式且每次调用都不重复的 18 位身份证号。 return "Bearer " + jwtService.generateToken(new AuthUser(10L, "employee", Set.of("EMPLOYEE"), departmentId));
* 结构:地区码(6) + 出生日期(8) + 顺序码(3) + 校验位(1)。
*/
private String uniqueIdNumber() {
int n = ID_SEQ.getAndIncrement() % 1000;
return String.format("41012319900101%03d4", n);
} }
private String maskedId(String idNumber) { private String createBody(String employeeNo, String name, String idNumber, Long departmentId) {
return idNumber.substring(0, 3) return """
+ "*".repeat(idNumber.length() - 6) {
+ idNumber.substring(idNumber.length() - 3); "employeeNo": "%s",
"name": "%s",
"idNumber": "%s",
"gender": 1,
"departmentId": %d,
"position": "Java开发工程师",
"hireDate": "2024-01-10",
"phone": "13800138000",
"email": "zhangsan@example.com",
"emergencyContact": "张父",
"emergencyPhone": "13900139000",
"status": "active",
"extra": {
"education": "本科",
"technicalLevel": "P6",
"salaryGrade": "S3",
"address": "北京市朝阳区"
}
}
""".formatted(employeeNo, name, idNumber, departmentId);
} }
private Map<String, Object> createBody(String employeeNo, String idNumber) { private void createEmployee(String employeeNo, String name, String idNumber, Long departmentId) throws Exception {
Map<String, Object> body = new LinkedHashMap<>(); mockMvc.perform(post("/api/v1/staff")
body.put("employeeNo", employeeNo); .header("Authorization", adminToken())
body.put("name", "张三");
body.put("idNumber", idNumber);
body.put("gender", 1);
body.put("departmentId", 1001L);
body.put("position", "Java开发工程师");
body.put("hireDate", "2024-01-10");
body.put("phone", "13800138000");
body.put("email", "zhangsan@example.com");
body.put("emergencyContact", "张父");
body.put("emergencyPhone", "13900139000");
Map<String, Object> extra = new LinkedHashMap<>();
extra.put("education", "本科");
extra.put("technicalLevel", "P6");
extra.put("salaryGrade", "S3");
extra.put("address", "北京市朝阳区");
body.put("extra", extra);
return body;
}
private long create(String token, String employeeNo) throws Exception {
return create(token, employeeNo, uniqueIdNumber());
}
private long create(String token, String employeeNo, String idNumber) throws Exception {
MvcResult result = mockMvc.perform(post("/api/v1/staff")
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createBody(employeeNo, idNumber)))) .content(createBody(employeeNo, name, idNumber, departmentId)))
.andExpect(status().isCreated()) .andExpect(status().isCreated())
.andExpect(jsonPath("$.code").value(201)) .andExpect(jsonPath("$.code").value(201));
.andExpect(jsonPath("$.data.employeeNo").value(employeeNo)) }
.andReturn();
return objectMapper.readTree(result.getResponse().getContentAsString()) private Long employeeId(String employeeNo) {
.path("data").path("id").asLong(); return employeeRepository.findByEmployeeNo(employeeNo).orElseThrow().getId();
} }
@Test @Test
void createListDetailFlow() throws Exception { void test01_createListAndDetail() throws Exception {
String admin = token("ADMIN", null); createEmployee("EMP20240001", "张三", "110101199001011234", 1001L);
String employeeNo = uniqueNo();
String idNumber = uniqueIdNumber();
long id = create(admin, employeeNo, idNumber);
mockMvc.perform(get("/api/v1/staff") mockMvc.perform(get("/api/v1/staff")
.header("Authorization", "Bearer " + admin) .header("Authorization", adminToken())
.param("keyword", employeeNo)) .param("page", "1").param("size", "20"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.totalElements").value(1)) .andExpect(jsonPath("$.data.totalElements").value(1))
.andExpect(jsonPath("$.data.content[0].employeeNo").value("EMP20240001"))
.andExpect(jsonPath("$.data.content[0].phone").value("138****8000")); .andExpect(jsonPath("$.data.content[0].phone").value("138****8000"));
mockMvc.perform(get("/api/v1/staff/{id}", id) Long id = employeeId("EMP20240001");
.header("Authorization", "Bearer " + admin)) mockMvc.perform(get("/api/v1/staff/" + id).header("Authorization", adminToken()))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.idNumber").value(maskedId(idNumber))) .andExpect(jsonPath("$.data.idNumber").value("110************234"))
.andExpect(jsonPath("$.data.phone").value("138****8000"))
.andExpect(jsonPath("$.data.emergencyPhone").value("13900139000"))
.andExpect(jsonPath("$.data.extra.technicalLevel").value("P6")) .andExpect(jsonPath("$.data.extra.technicalLevel").value("P6"))
.andExpect(jsonPath("$.data.emergencyPhone").value("13900139000")); .andExpect(jsonPath("$.data.extra.salaryGrade").value("S3"));
} }
@Test @Test
void duplicateEmployeeNoConflict() throws Exception { void test02_duplicateEmployeeNoReturns409() throws Exception {
String admin = token("ADMIN", null); createEmployee("EMP20240002", "李四", "110101199001011235", 1001L);
String employeeNo = uniqueNo();
create(admin, employeeNo);
mockMvc.perform(post("/api/v1/staff") mockMvc.perform(post("/api/v1/staff")
.header("Authorization", "Bearer " + admin) .header("Authorization", adminToken())
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createBody(employeeNo, uniqueIdNumber())))) .content(createBody("EMP20240002", "王五", "110101199001011236", 1002L)))
.andExpect(status().isConflict()) .andExpect(status().isConflict())
.andExpect(jsonPath("$.code").value(409)); .andExpect(jsonPath("$.code").value(409));
} }
@Test @Test
void employeeMaskingAndDataScope() throws Exception { void test03_employeeDataPermissionAndMasking() throws Exception {
String admin = token("ADMIN", null); createEmployee("EMP20240003", "赵六", "110101199001011237", 1001L);
String employeeNo = uniqueNo(); Long id = employeeId("EMP20240003");
String idNumber = uniqueIdNumber();
long id = create(admin, employeeNo, idNumber);
// 同部门员工:脱敏、敏感字段为 null // 本部门员工可查看,但敏感字段脱敏
String sameDeptEmp = token("EMPLOYEE", 1001L); mockMvc.perform(get("/api/v1/staff/" + id).header("Authorization", employeeToken(1001L)))
mockMvc.perform(get("/api/v1/staff/{id}", id)
.header("Authorization", "Bearer " + sameDeptEmp))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.idNumber").value(maskedId(idNumber))) .andExpect(jsonPath("$.data.idNumber").value("110************237"))
.andExpect(jsonPath("$.data.extra.technicalLevel").isEmpty()) .andExpect(jsonPath("$.data.phone").value("138****8000"))
.andExpect(jsonPath("$.data.emergencyPhone").value("139****9000")); .andExpect(jsonPath("$.data.emergencyPhone").value("139****9000"))
.andExpect(jsonPath("$.data.extra.technicalLevel").doesNotExist());
// 跨部门员工:禁止查看 // 跨部门员工无权限
String crossDeptEmp = token("EMPLOYEE", 1002L); mockMvc.perform(get("/api/v1/staff/" + id).header("Authorization", employeeToken(1002L)))
mockMvc.perform(get("/api/v1/staff/{id}", id) .andExpect(status().isForbidden())
.header("Authorization", "Bearer " + crossDeptEmp)) .andExpect(jsonPath("$.code").value(403));
.andExpect(status().isForbidden());
} }
@Test @Test
void updateAndChangeLog() throws Exception { void test04_updateGeneratesChangeLog() throws Exception {
String admin = token("ADMIN", null); createEmployee("EMP20240004", "孙七", "110101199001011238", 1001L);
String hr = token("HR", null); Long id = employeeId("EMP20240004");
String employeeNo = uniqueNo();
long id = create(admin, employeeNo);
Map<String, Object> update = new LinkedHashMap<>(); mockMvc.perform(put("/api/v1/staff/" + id)
update.put("position", "高级Java开发工程师"); .header("Authorization", adminToken())
mockMvc.perform(put("/api/v1/staff/{id}", id)
.header("Authorization", "Bearer " + hr)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(update))) .content("""
{"position": "高级Java开发工程师", "extra": {"technicalLevel": "P7"}}
"""))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.position").value("高级Java开发工程师")); .andExpect(jsonPath("$.data.position").value("高级Java开发工程师"));
mockMvc.perform(get("/api/v1/staff/{id}/change-logs", id) mockMvc.perform(get("/api/v1/staff/" + id + "/change-logs")
.header("Authorization", "Bearer " + admin)) .header("Authorization", adminToken()))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.content[0].changeType").value("POSITION_CHANGE")) .andExpect(jsonPath("$.data.totalElements").value(2))
.andExpect(jsonPath("$.data.content[0].newValue").value("高级Java开发工程师")); .andExpect(jsonPath("$.data.content[0].changeType").value("LEVEL_CHANGE"))
.andExpect(jsonPath("$.data.content[1].changeType").value("POSITION_CHANGE"));
} }
@Test @Test
void checkEmployeeNo() throws Exception { void test05_checkEmployeeNo() throws Exception {
String admin = token("ADMIN", null); createEmployee("EMP20240005", "周八", "110101199001011239", 1001L);
String employeeNo = uniqueNo();
create(admin, employeeNo);
mockMvc.perform(get("/api/v1/staff/check/employee-no") mockMvc.perform(get("/api/v1/staff/check/employee-no")
.header("Authorization", "Bearer " + admin) .header("Authorization", adminToken())
.param("employeeNo", employeeNo)) .param("employeeNo", "EMP20240005"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.exists").value(true)); .andExpect(jsonPath("$.data.exists").value(true));
mockMvc.perform(get("/api/v1/staff/check/employee-no") mockMvc.perform(get("/api/v1/staff/check/employee-no")
.header("Authorization", "Bearer " + admin) .header("Authorization", adminToken())
.param("employeeNo", "NOT_EXIST")) .param("employeeNo", "NOT_EXIST"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.exists").value(false)); .andExpect(jsonPath("$.data.exists").value(false));
} }
@Test @Test
void deleteFlow() throws Exception { void test06_logicalDeleteAndRepeatProtection() throws Exception {
String admin = token("ADMIN", null); createEmployee("EMP20240006", "吴九", "110101199001011240", 1001L);
String employeeNo = uniqueNo(); Long id = employeeId("EMP20240006");
long id = create(admin, employeeNo);
mockMvc.perform(delete("/api/v1/staff/{id}", id) mockMvc.perform(delete("/api/v1/staff/" + id).header("Authorization", adminToken()))
.header("Authorization", "Bearer " + admin))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("inactive")); .andExpect(jsonPath("$.data.status").value("inactive"));
// 重复删除应失败 mockMvc.perform(delete("/api/v1/staff/" + id).header("Authorization", adminToken()))
mockMvc.perform(delete("/api/v1/staff/{id}", id) .andExpect(status().isBadRequest())
.header("Authorization", "Bearer " + admin)) .andExpect(jsonPath("$.code").value(400));
.andExpect(status().isBadRequest());
} }
@Test @Test
void batchDeleteFlow() throws Exception { void test07_batchDelete() throws Exception {
String admin = token("ADMIN", null); createEmployee("EMP20240007", "郑十", "110101199001011241", 1001L);
long id1 = create(admin, uniqueNo()); createEmployee("EMP20240008", "钱一", "110101199001011242", 1001L);
long id2 = create(admin, uniqueNo()); Long id1 = employeeId("EMP20240007");
Long id2 = employeeId("EMP20240008");
Map<String, Object> body = new LinkedHashMap<>(); mockMvc.perform(post("/api/v1/staff/batch-delete")
body.put("ids", new long[]{id1, id2}); .header("Authorization", adminToken())
MvcResult result = mockMvc.perform(post("/api/v1/staff/batch-delete")
.header("Authorization", "Bearer " + admin)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(body))) .content("{\"ids\": [" + id1 + ", " + id2 + "]}"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andReturn(); .andExpect(jsonPath("$.data.successCount").value(2))
JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).path("data"); .andExpect(jsonPath("$.data.failedList.length()").value(0));
assertThat(data.path("successCount").asInt()).isEqualTo(2);
assertThat(data.path("failedList").size()).isEqualTo(0);
} }
@Test @Test
void auditLogsAndDepartments() throws Exception { void test08_auditLogsAndDepartments() throws Exception {
String admin = token("ADMIN", null); createEmployee("EMP20240009", "冯二", "110101199001011243", 1001L);
create(admin, uniqueNo());
mockMvc.perform(get("/api/v1/staff/audit-logs") mockMvc.perform(get("/api/v1/staff/audit-logs")
.header("Authorization", "Bearer " + admin)) .header("Authorization", adminToken())
.param("operation", "CREATE"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.totalElements").isNumber()); .andExpect(jsonPath("$.data.totalElements").value(1))
.andExpect(jsonPath("$.data.content[0].operation").value("CREATE"));
mockMvc.perform(get("/api/v1/staff/departments") mockMvc.perform(get("/api/v1/staff/departments").header("Authorization", adminToken()))
.header("Authorization", "Bearer " + admin))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data.length()").value(3)); .andExpect(jsonPath("$.data.length()").value(3));
} }