fix(aihr): bind candidate interview sessions to owner

This commit is contained in:
2026-07-14 09:59:44 +08:00
parent 91bd28811d
commit cd756150a1
6 changed files with 62 additions and 10 deletions
@@ -51,12 +51,12 @@ public class AihrInterviewController {
@PostMapping("/answer")
public R<AnswerResponse> answer(@RequestBody AnswerRequest request) {
return R.ok(interviewService.answer(request));
return R.ok(interviewService.answer(request, currentAppUsername()));
}
@PostMapping("/finish")
public R<FinishResponse> finish(@RequestBody FinishRequest request) {
return R.ok(interviewService.finish(request));
return R.ok(interviewService.finish(request, currentAppUsername()));
}
@GetMapping("/records")
@@ -64,8 +64,15 @@ public class AihrInterviewService {
}
public AnswerResponse answer(AnswerRequest request) {
return answer(request, "");
}
public AnswerResponse answer(AnswerRequest request, String ownerIdentity) {
String answerText = request == null ? "" : clean(request.answerText());
InterviewSession session = sessions.get(request == null ? null : request.sessionId());
if (!isBlank(ownerIdentity)) {
requireSessionOwner(session, ownerIdentity);
}
if (session != null && request != null && !isBlank(request.questionId())) {
session.answers().put(request.questionId(), answerText);
}
@@ -78,14 +85,24 @@ public class AihrInterviewService {
}
public FinishResponse finish(FinishRequest request) {
return finish(request, "");
}
public FinishResponse finish(FinishRequest request, String ownerIdentity) {
String sessionKey = request == null || isBlank(request.sessionId()) ? "iv-" + UUID.randomUUID() : clean(request.sessionId());
InterviewSession session = sessions.computeIfAbsent(
sessionKey,
key -> {
CandidateProfile candidate = resolveCandidate(null, null, null, key);
return new InterviewSession(candidate, candidate.questions(), new ConcurrentHashMap<>(), "local-rubric");
}
);
InterviewSession session;
if (isBlank(ownerIdentity)) {
session = sessions.computeIfAbsent(
sessionKey,
key -> {
CandidateProfile candidate = resolveCandidate(null, null, null, key);
return new InterviewSession(candidate, candidate.questions(), new ConcurrentHashMap<>(), "local-rubric");
}
);
} else {
session = sessions.get(sessionKey);
requireSessionOwner(session, ownerIdentity);
}
if (request != null && request.answers() != null) {
request.answers().forEach((questionId, answer) -> {
if (!isBlank(questionId)) {
@@ -570,6 +587,12 @@ public class AihrInterviewService {
return candidates.get("zhang-ming");
}
private static void requireSessionOwner(InterviewSession session, String ownerIdentity) {
if (session == null || !clean(ownerIdentity).equals(clean(session.candidate().id()))) {
throw new IllegalArgumentException("面试会话不存在或无权访问");
}
}
private CandidateProfile dynamicCandidate(String candidateId, String candidateName, String positionCode) {
CandidateProfile template = candidates.get("zhang-ming");
String cleanId = clean(candidateId);
@@ -3,6 +3,7 @@ package org.dromara.aihr.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.dromara.aihr.domain.AihrInterviewDto.CandidateLinkRequest;
import org.dromara.aihr.domain.AihrInterviewDto.CandidateLinkResponse;
import org.dromara.aihr.domain.AihrInterviewDto.AnswerRequest;
import org.dromara.aihr.domain.AihrInterviewDto.FinishRequest;
import org.dromara.aihr.domain.AihrInterviewDto.RecordResponse;
import org.dromara.aihr.domain.AihrInterviewDto.ReviewRequest;
@@ -174,4 +175,28 @@ class AihrInterviewServiceTest {
assertEquals("待复核", record.reviewStatus());
assertEquals(record.score(), record.finalScore());
}
@Test
@Tag("dev")
void appInterviewSessionRejectsForeignAndUnknownOwner() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
AihrModelSeedService modelService = mock(AihrModelSeedService.class);
when(modelService.tryChat(anyString(), anyString(), anyDouble())).thenReturn(Optional.empty());
AihrInterviewService service = new AihrInterviewService(new ObjectMapper(), modelService, jdbcTemplate);
var ownerSession = service.start(new StartRequest("candidate-a", "候选人A", "customer-service", "mobile"));
var otherSession = service.start(new StartRequest("candidate-b", "候选人B", "customer-service", "mobile"));
IllegalArgumentException foreign = assertThrows(IllegalArgumentException.class, () -> service.answer(
new AnswerRequest(otherSession.sessionId(), "q1", "越权回答", null), "candidate-a"
));
assertEquals("面试会话不存在或无权访问", foreign.getMessage());
IllegalArgumentException unknown = assertThrows(IllegalArgumentException.class, () -> service.finish(
new FinishRequest("iv-not-started", Map.of("q1", "越权完成")), "candidate-a"
));
assertEquals("面试会话不存在或无权访问", unknown.getMessage());
service.answer(new AnswerRequest(ownerSession.sessionId(), "q1", "本人回答", null), "candidate-a");
}
}