fix(aihr): prevent duplicate employee principal links

This commit is contained in:
2026-07-14 06:29:03 +08:00
parent 3540b072f3
commit e332efb731
5 changed files with 43 additions and 10 deletions
@@ -143,12 +143,7 @@ public class AihrInterviewService {
FROM aihr_candidate_employee_link
WHERE tenant_id = '000000' AND candidate_id = ?
LIMIT 1
""", (rs, rowNum) -> new CandidateLinkResponse(
rs.getString("candidate_id"),
rs.getString("ext_party_id"),
rs.getString("status"),
rs.getTimestamp("linked_time") == null ? "" : rs.getTimestamp("linked_time").toLocalDateTime().format(TIME_FORMAT)
), normalizedCandidateId).stream().findFirst().orElse(null);
""", this::mapCandidateLink, normalizedCandidateId).stream().findFirst().orElse(null);
}
public CandidateLinkResponse linkCandidate(CandidateLinkRequest request, String operator) {
@@ -171,6 +166,15 @@ public class AihrInterviewService {
if (activeEmployee == null || activeEmployee == 0) {
throw new IllegalArgumentException("员工主体不存在或已离职,请先同步在职组织快照");
}
CandidateLinkResponse existingPartyLink = jdbcTemplate.query("""
SELECT candidate_id, ext_party_id, status, linked_time
FROM aihr_candidate_employee_link
WHERE tenant_id = '000000' AND ext_party_id = ?
LIMIT 1
""", this::mapCandidateLink, extPartyId).stream().findFirst().orElse(null);
if (existingPartyLink != null && !candidateId.equals(existingPartyLink.candidateId())) {
throw new IllegalArgumentException("员工主体已关联其他候选人,请先由HR核对后处理");
}
CandidateLinkResponse current = candidateLink(candidateId);
if (current != null && !extPartyId.equals(current.extPartyId())) {
throw new IllegalArgumentException("候选人已关联其他员工主体,请先由HR核对后处理");
@@ -198,6 +202,15 @@ public class AihrInterviewService {
);
}
private CandidateLinkResponse mapCandidateLink(ResultSet rs, int rowNum) throws SQLException {
return new CandidateLinkResponse(
rs.getString("candidate_id"),
rs.getString("ext_party_id"),
rs.getString("status"),
rs.getTimestamp("linked_time") == null ? "" : rs.getTimestamp("linked_time").toLocalDateTime().format(TIME_FORMAT)
);
}
private void saveResult(String sessionId, InterviewSession session, ScoreResult score) {
ensureResultTable();
jdbcTemplate.update("""
@@ -303,7 +316,7 @@ public class AihrInterviewService {
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_aihr_candidate_employee_link_candidate` (`tenant_id`, `candidate_id`),
KEY `idx_aihr_candidate_employee_link_party` (`tenant_id`, `ext_party_id`)
UNIQUE KEY `uk_aihr_candidate_employee_link_party` (`tenant_id`, `ext_party_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 候选人与员工主体关联';
""");
candidateLinkTableReady = true;
@@ -6,6 +6,7 @@ import org.dromara.aihr.domain.AihrInterviewDto.CandidateLinkResponse;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
@@ -53,6 +54,25 @@ class AihrInterviewServiceTest {
verify(jdbcTemplate, never()).update(anyString(), eq("candidate-2"), eq("employee-new"), eq("hr"));
}
@Test
@Tag("dev")
void linkCandidateRejectsAnEmployeeAlreadyLinkedToAnotherCandidate() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
AihrInterviewService service = spy(new AihrInterviewService(new ObjectMapper(), null, jdbcTemplate));
CandidateLinkResponse existing = new CandidateLinkResponse("candidate-old", "employee-4", "已关联", "07-14 06:00");
doReturn(null).when(service).candidateLink("candidate-4");
when(jdbcTemplate.queryForObject(anyString(), eq(Integer.class), eq("employee-4"))).thenReturn(1);
when(jdbcTemplate.query(anyString(), org.mockito.ArgumentMatchers.<RowMapper<CandidateLinkResponse>>any(), eq("employee-4")))
.thenReturn(java.util.List.of(existing));
IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> service.linkCandidate(
new CandidateLinkRequest("candidate-4", "employee-4"), "hr"
));
assertEquals("员工主体已关联其他候选人,请先由HR核对后处理", error.getMessage());
verify(jdbcTemplate, never()).update(anyString(), eq("candidate-4"), eq("employee-4"), eq("hr"));
}
@Test
@Tag("dev")
void linkCandidateIsIdempotentForTheSameEmployee() {
@@ -38,5 +38,5 @@ CREATE TABLE IF NOT EXISTS `aihr_candidate_employee_link` (
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_aihr_candidate_employee_link_candidate` (`tenant_id`, `candidate_id`),
KEY `idx_aihr_candidate_employee_link_party` (`tenant_id`, `ext_party_id`)
UNIQUE KEY `uk_aihr_candidate_employee_link_party` (`tenant_id`, `ext_party_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 候选人与员工主体关联';