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