feat(aihr): add candidate voice interview answers
This commit is contained in:
+8
-2
@@ -17,7 +17,10 @@ public final class AihrInterviewDto {
|
||||
public record QuestionResponse(String questionId, Integer seq, String questionText, String point, String scenario, String sampleAnswer) {
|
||||
}
|
||||
|
||||
public record AnswerRequest(String sessionId, String questionId, String answerText, String answerAudioUrl) {
|
||||
public record AnswerRequest(String sessionId, String questionId, String answerText, String answerAudioUrl, Long answerAudioOssId) {
|
||||
public AnswerRequest(String sessionId, String questionId, String answerText, String answerAudioUrl) {
|
||||
this(sessionId, questionId, answerText, answerAudioUrl, null);
|
||||
}
|
||||
}
|
||||
|
||||
public record AnswerResponse(Integer score, Map<String, Integer> dimensions, String aiComment) {
|
||||
@@ -50,7 +53,10 @@ public final class AihrInterviewDto {
|
||||
public record DimensionResponse(String label, Object value, String note) {
|
||||
}
|
||||
|
||||
public record QuestionRecordResponse(Integer seq, String question, String answer, String rating) {
|
||||
public record QuestionRecordResponse(Integer seq, String question, String answer, String rating, Long audioOssId) {
|
||||
public QuestionRecordResponse(Integer seq, String question, String answer, String rating) {
|
||||
this(seq, question, answer, rating, null);
|
||||
}
|
||||
}
|
||||
|
||||
public record RecordResponse(
|
||||
|
||||
+34
-4
@@ -19,6 +19,7 @@ import org.dromara.aihr.domain.AihrInterviewDto.RecordResponse;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.ReviewRequest;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.StartRequest;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.StartResponse;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -59,7 +60,7 @@ public class AihrInterviewService {
|
||||
);
|
||||
GeneratedQuestions generated = generateQuestions(candidate).orElseGet(() -> new GeneratedQuestions(candidate.questions(), "local-rubric"));
|
||||
String sessionId = "iv-" + UUID.randomUUID();
|
||||
sessions.put(sessionId, new InterviewSession(tenantId(), candidate, generated.questions(), new ConcurrentHashMap<>(), generated.source()));
|
||||
sessions.put(sessionId, new InterviewSession(tenantId(), candidate, generated.questions(), new ConcurrentHashMap<>(), new ConcurrentHashMap<>(), generated.source()));
|
||||
return new StartResponse(sessionId, candidate.id(), candidate.name(), candidate.position(), generated.questions(), generated.source());
|
||||
}
|
||||
|
||||
@@ -74,9 +75,15 @@ public class AihrInterviewService {
|
||||
if (!isBlank(ownerIdentity)) {
|
||||
requireSessionOwner(session, ownerIdentity);
|
||||
}
|
||||
validateAnswerAudioOwnership(request == null ? null : request.answerAudioOssId(), ownerIdentity);
|
||||
if (session != null && request != null && !isBlank(request.questionId())) {
|
||||
requireKnownQuestion(session, request.questionId());
|
||||
session.answers().put(request.questionId(), answerText);
|
||||
if (request.answerAudioOssId() == null) {
|
||||
session.answerAudioOssIds().remove(request.questionId());
|
||||
} else {
|
||||
session.answerAudioOssIds().put(request.questionId(), request.answerAudioOssId());
|
||||
}
|
||||
}
|
||||
int baseScore = answerText.isBlank() ? 0 : Math.min(92, 62 + Math.min(answerText.length() / 4, 30));
|
||||
return new AnswerResponse(
|
||||
@@ -98,7 +105,7 @@ public class AihrInterviewService {
|
||||
sessionKey,
|
||||
key -> {
|
||||
CandidateProfile candidate = resolveCandidate(null, null, null, key);
|
||||
return new InterviewSession(tenantId(), candidate, candidate.questions(), new ConcurrentHashMap<>(), "local-rubric");
|
||||
return new InterviewSession(tenantId(), candidate, candidate.questions(), new ConcurrentHashMap<>(), new ConcurrentHashMap<>(), "local-rubric");
|
||||
}
|
||||
);
|
||||
} else {
|
||||
@@ -463,7 +470,8 @@ public class AihrInterviewService {
|
||||
return session.questions().stream().map(question -> {
|
||||
String answer = clean(session.answers().get(question.questionId()));
|
||||
String rating = answer.length() >= 32 ? "优秀" : answer.length() >= 12 ? "良好" : "待提升";
|
||||
return new QuestionRecordResponse(question.seq(), question.questionText(), answer.isBlank() ? "未回答" : answer, rating);
|
||||
return new QuestionRecordResponse(question.seq(), question.questionText(), answer.isBlank() ? "未回答" : answer, rating,
|
||||
session.answerAudioOssIds().get(question.questionId()));
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -641,6 +649,28 @@ public class AihrInterviewService {
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAnswerAudioOwnership(Long audioOssId, String ownerIdentity) {
|
||||
if (audioOssId == null) {
|
||||
return;
|
||||
}
|
||||
if (isBlank(ownerIdentity)) {
|
||||
throw new ServiceException("录音归属校验失败,请重新录音后提交");
|
||||
}
|
||||
try {
|
||||
Integer count = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM aihr_practice_audio_upload
|
||||
WHERE tenant_id = ? AND oss_id = ? AND owner_identity = ?
|
||||
AND status IN ('staged', 'bound')
|
||||
""", Integer.class, tenantId(), audioOssId, ownerIdentity.trim());
|
||||
if (count == null || count == 0) {
|
||||
throw new ServiceException("录音归属校验失败,请重新录音后提交");
|
||||
}
|
||||
} catch (DataAccessException error) {
|
||||
throw new ServiceException("录音归属校验失败,请重新录音后提交");
|
||||
}
|
||||
}
|
||||
|
||||
private CandidateProfile dynamicCandidate(String candidateId, String candidateName, String positionCode) {
|
||||
CandidateProfile template = candidates.get("zhang-ming");
|
||||
String cleanId = clean(candidateId);
|
||||
@@ -758,7 +788,7 @@ public class AihrInterviewService {
|
||||
}
|
||||
|
||||
private record InterviewSession(String tenantId, CandidateProfile candidate, List<QuestionResponse> questions,
|
||||
Map<String, String> answers, String source) {
|
||||
Map<String, String> answers, Map<String, Long> answerAudioOssIds, String source) {
|
||||
}
|
||||
|
||||
private record GeneratedQuestions(List<QuestionResponse> questions, String source) {
|
||||
|
||||
+20
@@ -8,6 +8,7 @@ import org.dromara.aihr.domain.AihrInterviewDto.FinishRequest;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.RecordResponse;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.ReviewRequest;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.StartRequest;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
@@ -334,6 +335,25 @@ class AihrInterviewServiceTest {
|
||||
service.answer(new AnswerRequest(ownerSession.sessionId(), "q1", "本人回答", null), "candidate-a");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
void interviewAnswerRejectsAudioOwnedByAnotherIdentity() {
|
||||
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 started = service.start(new StartRequest("candidate-audio", "候选人", "customer-service", "mobile"));
|
||||
when(jdbcTemplate.queryForObject(anyString(), eq(Integer.class), eq("000000"), eq(9002L), eq("candidate-audio")))
|
||||
.thenReturn(0);
|
||||
|
||||
ServiceException error = assertThrows(ServiceException.class, () -> service.answer(
|
||||
new AnswerRequest(started.sessionId(), "q1", "语音回答", null, 9002L), "candidate-audio"
|
||||
));
|
||||
|
||||
assertEquals("录音归属校验失败,请重新录音后提交", error.getMessage());
|
||||
verify(jdbcTemplate).queryForObject(anyString(), eq(Integer.class), eq("000000"), eq(9002L), eq("candidate-audio"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
void interviewAnswerRejectsUnknownSessionForSystemFlow() {
|
||||
|
||||
Reference in New Issue
Block a user