fix(aihr): validate candidate links

This commit is contained in:
2026-07-14 10:46:39 +08:00
parent 92afb51902
commit 94a3d0fe5b
4 changed files with 41 additions and 0 deletions
@@ -238,6 +238,9 @@ public class AihrInterviewService {
if (current != null && !extPartyId.equals(current.extPartyId())) {
throw new IllegalArgumentException("候选人已关联其他员工主体,请先由HR核对后处理");
}
if (current == null && !candidateExists(candidateId)) {
throw new IllegalArgumentException("候选人不存在,请先创建或完成一次面试记录");
}
jdbcTemplate.update("""
INSERT INTO aihr_candidate_employee_link
(tenant_id, candidate_id, ext_party_id, status, linked_by, linked_time, create_time, update_time)
@@ -248,6 +251,25 @@ public class AihrInterviewService {
return candidateLink(candidateId);
}
private boolean candidateExists(String candidateId) {
if (candidates.containsKey(candidateId)
|| sessions.values().stream().anyMatch(session -> candidateId.equals(session.candidate().id()))) {
return true;
}
try {
ensureResultTable();
Integer count = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM aihr_interview_result
WHERE tenant_id = ? AND candidate_id = ?
""", Integer.class, tenantId(), candidateId);
return count != null && count > 0;
} catch (DataAccessException e) {
log.warn("resolve interview candidate failed(处理错误已隐藏)");
return false;
}
}
private RecordResponse mapRecord(ResultSet rs, int rowNum) throws SQLException {
var finishedTime = rs.getTimestamp("finished_time");
Integer aiScore = rs.getInt("total_score");
@@ -103,6 +103,23 @@ class AihrInterviewServiceTest {
verify(jdbcTemplate).update(anyString(), eq("000000"), eq("candidate-3"), eq("employee-3"), eq("hr"));
}
@Test
@Tag("dev")
void linkCandidateRejectsUnknownCandidate() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
AihrInterviewService service = spy(new AihrInterviewService(new ObjectMapper(), null, jdbcTemplate));
doReturn(null).when(service).candidateLink("candidate-unknown");
when(jdbcTemplate.queryForObject(anyString(), eq(Integer.class), eq("000000"), eq("employee-5"))).thenReturn(1);
when(jdbcTemplate.queryForObject(anyString(), eq(Integer.class), eq("000000"), eq("candidate-unknown"))).thenReturn(0);
IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> service.linkCandidate(
new CandidateLinkRequest("candidate-unknown", "employee-5"), "hr"
));
assertEquals("候选人不存在,请先创建或完成一次面试记录", error.getMessage());
verify(jdbcTemplate, never()).update(anyString(), eq("000000"), eq("candidate-unknown"), eq("employee-5"), eq("hr"));
}
@Test
@Tag("dev")
void interviewRecordsAreScopedToCurrentTenant() {
+1
View File
@@ -177,3 +177,4 @@
- 2026-07-14 BRD 4.4 每日三题资格补强:每日派题现在在组织快照存在 `hire_date` 列时按“入职日期至今不超过 3 个月”判断,边界按自然月且包含当天;旧 Demo 快照没有该列时继续使用“移动端训练记录少于 3 次”的兼容回退。正式试点仍需在外部组织同步落地并校验 `hire_date`,不能把回退口径当成正式新员工资格。
- 2026-07-14 BRD G3 候选资料审核状态复核:候选人资料审核接口此前在空请求体时默认写回“待审核”,可能覆盖已有“已通过/已驳回”状态;现要求审核动作显式提交三态之一,空值和未知状态直接拒绝,避免后台误操作回退审核结论。
- 2026-07-14 BRD 案例署名复核:案例库已有负责人字段,但此前上传、整理和入库记录都硬编码为“培训组”,无法追溯案例贡献者;现复用既有 `owner` 字段保存首次上传身份,整理和入库阶段保留原署名,手机号身份仅显示员工末四位,避免把原始手机号写入案例展示。
- 2026-07-14 BRD 候选人关联完整性复核:候选人—员工关联接口此前只校验员工主体在职,允许给不存在的任意 `candidateId` 建立入职关联;现要求候选人必须来自内置候选档案、活动面试会话或当前租户已持久化面试记录,保留已有幂等关联和历史数据兼容。
+1
View File
@@ -515,6 +515,7 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "media_oss_id"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "storeSourceAudio"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "currentCaseOwner"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrInterviewService.java "候选人不存在,请先创建或完成一次面试记录"
contains mobile-uni/src/pages/user/cases/index.vue "case-audio"
contains frontend/src/views/knowledge/cases.vue "case-audio"
contains backend/script/sql/aihr_practice_mysql8.sql "aihr_case_record"