feat(aihr): persist interview human review

This commit is contained in:
2026-07-14 09:44:03 +08:00
parent ccb164a45a
commit e55abec630
10 changed files with 271 additions and 14 deletions
@@ -12,6 +12,7 @@ import org.dromara.aihr.domain.AihrInterviewDto.CandidateLinkResponse;
import org.dromara.aihr.domain.AihrInterviewDto.FinishRequest;
import org.dromara.aihr.domain.AihrInterviewDto.FinishResponse;
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.aihr.service.AihrInterviewService;
@@ -22,6 +23,7 @@ import org.dromara.common.core.enums.UserType;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -70,6 +72,15 @@ public class AihrInterviewController {
return R.ok(interviewService.records(limit, appUsername.isBlank() ? candidateId : appUsername));
}
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
@PostMapping("/records/{sessionId}/review")
public R<Void> review(@PathVariable String sessionId, @RequestBody ReviewRequest request) {
LoginUser loginUser = LoginHelper.getLoginUser();
String operator = loginUser == null ? "unknown" : loginUser.getUsername();
interviewService.reviewResult(sessionId, request, operator);
return R.ok();
}
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
@GetMapping("/candidate-links")
public R<CandidateLinkResponse> candidateLink(@RequestParam String candidateId) {
@@ -26,6 +26,9 @@ public final class AihrInterviewDto {
public record FinishRequest(String sessionId, Map<String, String> answers) {
}
public record ReviewRequest(Integer reviewedScore, String reviewNote) {
}
public record CandidateLinkRequest(String candidateId, String extPartyId) {
}
@@ -50,7 +53,24 @@ public final class AihrInterviewDto {
public record QuestionRecordResponse(Integer seq, String question, String answer, String rating) {
}
public record RecordResponse(String time, String candidate, String position, Integer score, String result, String summary,
List<QuestionRecordResponse> questions) {
public record RecordResponse(
String sessionId,
String time,
String candidate,
String position,
Integer score,
Integer reviewedScore,
Integer finalScore,
String reviewStatus,
String reviewer,
String reviewNote,
String result,
String summary,
List<QuestionRecordResponse> questions
) {
public RecordResponse(String time, String candidate, String position, Integer score, String result, String summary,
List<QuestionRecordResponse> questions) {
this("", time, candidate, position, score, null, score, "待复核", "", "", result, summary, questions);
}
}
}
@@ -16,6 +16,7 @@ import org.dromara.aihr.domain.AihrInterviewDto.FinishResponse;
import org.dromara.aihr.domain.AihrInterviewDto.QuestionResponse;
import org.dromara.aihr.domain.AihrInterviewDto.QuestionRecordResponse;
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.springframework.jdbc.core.JdbcTemplate;
@@ -117,7 +118,8 @@ public class AihrInterviewService {
ensureResultTable();
int safeLimit = Math.max(1, Math.min(limit <= 0 ? 20 : limit, 100));
StringBuilder sql = new StringBuilder("""
SELECT finished_time, candidate_name, position_code, total_score, result, ai_summary, questions_json
SELECT session_id, finished_time, candidate_name, position_code, total_score,
reviewed_score, review_status, reviewer, review_note, result, ai_summary, questions_json
FROM aihr_interview_result
WHERE tenant_id = ?
""");
@@ -134,6 +136,30 @@ public class AihrInterviewService {
return jdbcTemplate.query(sql.toString(), this::mapRecord, args.toArray());
}
public void reviewResult(String sessionId, ReviewRequest request, String operator) {
ensureResultTable();
String normalizedSessionId = clean(sessionId);
if (normalizedSessionId.isBlank() || normalizedSessionId.length() > 100) {
throw new IllegalArgumentException("面试会话ID不能为空且不能超过100个字符");
}
Integer reviewedScore = request == null ? null : request.reviewedScore();
if (reviewedScore == null || reviewedScore < 0 || reviewedScore > 100) {
throw new IllegalArgumentException("人工复核分必须是0到100之间的整数");
}
String reviewNote = clean(request.reviewNote());
if (reviewNote.length() > 1000) {
throw new IllegalArgumentException("人工复核意见不能超过1000个字符");
}
int updated = jdbcTemplate.update("""
UPDATE aihr_interview_result
SET reviewed_score = ?, review_status = '已复核', reviewer = ?, review_note = ?, reviewed_time = now(), update_time = now()
WHERE tenant_id = ? AND session_id = ?
""", reviewedScore, firstNonBlank(operator, "unknown"), reviewNote, tenantId(), normalizedSessionId);
if (updated == 0) {
throw new IllegalArgumentException("面试记录不存在或不属于当前租户");
}
}
public CandidateLinkResponse candidateLink(String candidateId) {
ensureCandidateLinkTable();
String normalizedCandidateId = clean(candidateId);
@@ -193,11 +219,23 @@ public class AihrInterviewService {
private RecordResponse mapRecord(ResultSet rs, int rowNum) throws SQLException {
var finishedTime = rs.getTimestamp("finished_time");
Integer aiScore = rs.getInt("total_score");
if (rs.wasNull()) {
aiScore = null;
}
Object reviewedScoreValue = rs.getObject("reviewed_score");
Integer reviewedScore = reviewedScoreValue instanceof Number number ? number.intValue() : null;
return new RecordResponse(
rs.getString("session_id"),
finishedTime == null ? "" : finishedTime.toLocalDateTime().format(TIME_FORMAT),
rs.getString("candidate_name"),
rs.getString("position_code"),
rs.getInt("total_score"),
aiScore,
reviewedScore,
reviewedScore == null ? aiScore : reviewedScore,
firstNonBlank(rs.getString("review_status"), "待复核"),
firstNonBlank(rs.getString("reviewer"), ""),
firstNonBlank(rs.getString("review_note"), ""),
rs.getString("result"),
rs.getString("ai_summary"),
parseQuestionRecords(rs.getString("questions_json"))
@@ -274,6 +312,11 @@ public class AihrInterviewService {
`candidate_name` varchar(100) DEFAULT NULL COMMENT '候选人姓名',
`position_code` varchar(120) DEFAULT NULL COMMENT '岗位',
`total_score` int DEFAULT NULL COMMENT '总分',
`reviewed_score` int DEFAULT NULL COMMENT '人工复核分',
`review_status` varchar(20) DEFAULT '待复核' COMMENT '人工复核状态',
`reviewer` varchar(100) DEFAULT NULL COMMENT '复核人',
`review_note` varchar(1000) DEFAULT NULL COMMENT '人工复核意见',
`reviewed_time` datetime DEFAULT NULL COMMENT '复核时间',
`result` varchar(40) DEFAULT NULL COMMENT '结论',
`suggestion` varchar(500) DEFAULT NULL COMMENT '建议',
`ai_summary` varchar(1000) DEFAULT NULL COMMENT 'AI摘要',
@@ -291,17 +334,37 @@ public class AihrInterviewService {
KEY `idx_aihr_interview_finished` (`tenant_id`, `finished_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 面试结果';
""");
try {
jdbcTemplate.execute("ALTER TABLE aihr_interview_result ADD COLUMN questions_json text DEFAULT NULL COMMENT '面试问题与回答快照JSON' AFTER answers_json");
} catch (DataAccessException error) {
if (!String.valueOf(error.getMessage()).contains("Duplicate column")) {
throw error;
}
}
addColumnIfMissing("ALTER TABLE aihr_interview_result ADD COLUMN questions_json text DEFAULT NULL COMMENT '面试问题与回答快照JSON' AFTER answers_json");
addColumnIfMissing("ALTER TABLE aihr_interview_result ADD COLUMN reviewed_score int DEFAULT NULL COMMENT '人工复核分' AFTER total_score");
addColumnIfMissing("ALTER TABLE aihr_interview_result ADD COLUMN review_status varchar(20) DEFAULT '待复核' COMMENT '人工复核状态' AFTER reviewed_score");
addColumnIfMissing("ALTER TABLE aihr_interview_result ADD COLUMN reviewer varchar(100) DEFAULT NULL COMMENT '复核人' AFTER review_status");
addColumnIfMissing("ALTER TABLE aihr_interview_result ADD COLUMN review_note varchar(1000) DEFAULT NULL COMMENT '人工复核意见' AFTER reviewer");
addColumnIfMissing("ALTER TABLE aihr_interview_result ADD COLUMN reviewed_time datetime DEFAULT NULL COMMENT '复核时间' AFTER review_note");
tableReady = true;
}
}
private void addColumnIfMissing(String ddl) {
try {
jdbcTemplate.execute(ddl);
} catch (DataAccessException error) {
if (!isDuplicateColumnError(error)) {
throw error;
}
}
}
private boolean isDuplicateColumnError(DataAccessException error) {
Throwable current = error;
while (current != null) {
if (String.valueOf(current.getMessage()).contains("Duplicate column")) {
return true;
}
current = current.getCause();
}
return false;
}
private void ensureCandidateLinkTable() {
if (candidateLinkTableReady) {
return;
@@ -4,6 +4,7 @@ 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.RecordResponse;
import org.dromara.aihr.domain.AihrInterviewDto.ReviewRequest;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -120,4 +121,31 @@ class AihrInterviewServiceTest {
assertTrue(controllerSource.contains("StpUtil.hasRoleOr(TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE)"));
assertTrue(controllerSource.contains("return R.fail(\"无权查看面试记录\")"));
}
@Test
@Tag("dev")
void reviewResultPersistsHumanScoreWithoutChangingAiScore() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
AihrInterviewService service = new AihrInterviewService(new ObjectMapper(), null, jdbcTemplate);
when(jdbcTemplate.update(anyString(), eq(88), eq("hr-operator"), eq("人工确认现场协调能力不足"), eq("000000"), eq("iv-1")))
.thenReturn(1);
service.reviewResult("iv-1", new ReviewRequest(88, "人工确认现场协调能力不足"), "hr-operator");
verify(jdbcTemplate).update(anyString(), eq(88), eq("hr-operator"), eq("人工确认现场协调能力不足"), eq("000000"), eq("iv-1"));
}
@Test
@Tag("dev")
void reviewResultRejectsOutOfRangeScore() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
AihrInterviewService service = new AihrInterviewService(new ObjectMapper(), null, jdbcTemplate);
IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> service.reviewResult(
"iv-1", new ReviewRequest(101, "超出范围"), "hr-operator"
));
assertEquals("人工复核分必须是0到100之间的整数", error.getMessage());
verify(jdbcTemplate, never()).update(anyString(), eq(101), eq("hr-operator"), eq("超出范围"), eq("000000"), eq("iv-1"));
}
}
@@ -9,6 +9,11 @@ CREATE TABLE IF NOT EXISTS `aihr_interview_result` (
`candidate_name` varchar(100) DEFAULT NULL COMMENT '候选人姓名',
`position_code` varchar(120) DEFAULT NULL COMMENT '岗位',
`total_score` int DEFAULT NULL COMMENT '总分',
`reviewed_score` int DEFAULT NULL COMMENT '人工复核分',
`review_status` varchar(20) DEFAULT '待复核' COMMENT '人工复核状态',
`reviewer` varchar(100) DEFAULT NULL COMMENT '复核人',
`review_note` varchar(1000) DEFAULT NULL COMMENT '人工复核意见',
`reviewed_time` datetime DEFAULT NULL COMMENT '复核时间',
`result` varchar(40) DEFAULT NULL COMMENT '结论',
`suggestion` varchar(500) DEFAULT NULL COMMENT '建议',
`ai_summary` varchar(1000) DEFAULT NULL COMMENT 'AI摘要',
+1 -1
View File
@@ -6,7 +6,7 @@
| 页面 | 后端接口 | 处理 |
|---|---|---|
| AI面试 `/recruit/interview` | `POST /api/recruit/interview/start`、`/answer`、`/finish` | 已接入真实模型优先链路:配置 chat 模型后 `/start` 动态生成面试题,`/finish` 按前端提交的真实回答做结构化评分;未配置模型时使用本地 Rubric 估分,不返回固定候选人分数 |
| AI面试 `/recruit/interview` | `POST /api/recruit/interview/start`、`/answer`、`/finish`、`GET /records`、`POST /records/{sessionId}/review` | 已接入真实模型优先链路:配置 chat 模型后 `/start` 动态生成面试题,`/finish` 按前端提交的真实回答做结构化评分;未配置模型时使用本地 Rubric 估分,不返回固定候选人分数。AI 分数只作辅助参考,HR/管理员可提交 `reviewedScore/reviewNote` 完成人工复核;记录同时保留 AI 分、人工复核分、最终采用分和复核状态 |
| 候选人入职主体关联 `/recruit/interview` | `GET/POST /api/recruit/interview/candidate-links` | HR/管理员在当前租户范围内把本地候选人 ID 关联到已同步的在职 `ext_party_id`;只保存外部主体 ID,不复制姓名、部门等组织字段,重复关联同一主体幂等,候选人更换主体或同一主体已关联其他候选人会拒绝 |
| 三角色对练 `/train/practice` | `POST /api/train/practice/start`、`/turn`、`/finish` | 已接入编排 API;数据库启用 chat 模型后,`/turn` 客户回复按人设走真 LLM 生成(seed 剧本作剧情锚点),`/finish` 走单次 temperature=0 结构化评分(4 维分+导师改写+点评);模型未配置或调用失败自动回退 seed,契约不变 |
| 正式试点数据导出 | `GET /api/train/practice/export?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD` | 起止日期必填且包含结束日;只统计窗口内能通过唯一手机号或外部 ID 映射到在职组织快照的正式会话,排除重复手机号和身份碰撞。完训定义为每人至少 10 次,校准必须关联同一窗口内正式会话;CSV 同时给出校准命中数、SOP 可用数、满意度响应数/平均分,以及明细级 AI 分、人工校准分、校准人、校准时间、最终采用分、满意度分和意见,避免用四舍五入后的比率反推门禁状态;汇总和明细均携带正式人员及项目口径,不混入历史 seed/开发身份 |
+1 -1
View File
@@ -15,7 +15,7 @@
| 员工端今日/练/问/我 | `mobile-uni/src/pages/user/*`;员工首页、训练前预习、独立练习路由已实现 | 2026-07-14 已同步线上 H5;线上主 JS 与本地构建 SHA-256 一致,学习页已显示错题本和原因筛选,后续只做发布后回归 |
| 主管工作台/复盘/派发 | `mobile-uni/src/pages/supervisor/*`;线上工作台已完成视觉回归 | 页面主链路已在位;正式主管身份仍依赖组织数据 |
| 训练音频对象权限 | `AihrMobileController`、`AihrPracticeSeedService`;`GET /api/aihr/mobile/oss/{ossId}` | 已按员工本人、主管项目范围和系统用户区分音频读取权限;本地真实 HTTP 验证:归属员工 200,其他员工 403;生产正式主管范围仍需组织同步数据支撑 |
| 候选人面试与 HR 复核 | `mobile-uni/src/pages/candidate/*`、`frontend/src/views/recruit/interview.vue` | 工程闭环已在位;候选人资料移动端上传/列表现在只接受 APP 候选人本人身份,HR 继续通过独立审核接口处理;需继续保留人工复核证据 |
| 候选人面试与 HR 复核 | `mobile-uni/src/pages/candidate/*`、`frontend/src/views/recruit/interview.vue`、`AihrInterviewController` | 候选人资料移动端上传/列表只接受 APP 候选人本人身份,HR 通过独立审核接口处理;AI 面试结果现已持久化 `待复核/已复核`、AI 分、人工复核分、最终采用分、复核人、意见和时间,管理端可提交人工复核;正式试点仍需抽查复核证据 |
| SOP/RAG/资料处理 | `frontend/src/views/knowledge/*`、`backend/.../AihrSopSeedService.java` | 搜索、引用、反馈、异步处理和评审入口已在位 |
| 对练/每日题/主管带教 | `backend/.../AihrPracticeSeedService.java`、`mobile-uni/src/pages/user/practice` | 每日三题现支持文字或录音转写,语音 `audioUrl/ossId` 随作业提交并持久化;普通陪练、即时反馈、低分再练、错题归因和主管带教已在位,正式窗口仍需验证真实音频与低分样本 |
| 案例上传/整理/筛选 | `backend/.../AihrCaseService.java`、`mobile-uni/src/pages/user/cases`、`frontend/src/views/knowledge/cases.vue` | 上传与整理已在位,案例状态在内存缺失时可从 `aihr_case_record` 回读恢复;APP 用户的读取按项目范围校验,写入接口已增加“主管/项目经理/系统用户”门禁,普通员工不能提交、整理或入库案例;原始音频现在上传到 `sys_oss` 并在 `aihr_case_record.media_oss_id/media_url` 留痕,员工端和管理端详情均可回放;本轮补上管理端上传的项目选择与后端无项目拒绝,避免案例落成员工不可见的无范围记录;当前仓库仍没有可用预渲染视频样片资产,也没有稳定的学习点/主管点评模型,不能用音频回放冒充完整案例学习闭环 |
+20
View File
@@ -64,14 +64,25 @@ export type InterviewFinishResponse = {
};
export type InterviewRecord = {
sessionId?: string;
time: string;
candidate: string;
position: string;
score: number;
reviewedScore?: number | null;
finalScore?: number;
reviewStatus?: string;
reviewer?: string;
reviewNote?: string;
result: string;
summary: string;
};
export type InterviewReviewRequest = {
reviewedScore: number;
reviewNote?: string;
};
export type CandidateLinkResponse = {
candidateId: string;
extPartyId: string;
@@ -126,6 +137,15 @@ export function listInterviewRecords(limit = 20): Promise<ApiResult<InterviewRec
});
}
export function reviewInterviewRecord(sessionId: string, data: InterviewReviewRequest): Promise<ApiResult<null>> {
return request({
url: `/api/recruit/interview/records/${encodeURIComponent(sessionId)}/review`,
method: 'post',
data,
headers: { repeatSubmit: false }
});
}
export function getCandidateEmployeeLink(candidateId: string): Promise<ApiResult<CandidateLinkResponse | null>> {
return request({
url: '/api/recruit/interview/candidate-links',
+107 -1
View File
@@ -174,12 +174,56 @@
<el-table-column prop="time" label="时间" width="128" />
<el-table-column prop="candidate" label="候选人" width="120" />
<el-table-column prop="position" label="岗位" min-width="150" />
<el-table-column prop="score" label="得分" width="80" />
<el-table-column prop="score" label="AI得分" width="90" />
<el-table-column label="人工复核分" width="110">
<template #default="{ row }">{{ row.reviewedScore ?? '—' }}</template>
</el-table-column>
<el-table-column label="最终采用分" width="110">
<template #default="{ row }">{{ row.finalScore ?? row.score }}</template>
</el-table-column>
<el-table-column prop="result" label="结论" width="120" />
<el-table-column label="复核状态" width="100">
<template #default="{ row }">
<el-tag :type="row.reviewStatus === '已复核' ? 'success' : 'warning'" size="small">
{{ row.reviewStatus || '待复核' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="summary" label="AI 摘要" min-width="260" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :disabled="!row.sessionId" @click="openInterviewReview(row)">人工复核</el-button>
</template>
</el-table-column>
</el-table>
</section>
<el-dialog v-model="interviewReviewVisible" title="人工复核 AI 面试结果" width="520px">
<div v-if="reviewingRecord" class="interview-review-context">
<strong>{{ reviewingRecord.candidate }} · {{ reviewingRecord.position }}</strong>
<span>AI 得分 {{ reviewingRecord.score }},当前最终采用分 {{ reviewingRecord.finalScore ?? reviewingRecord.score }}</span>
</div>
<el-form label-width="104px" class="interview-review-form">
<el-form-item label="人工复核分" required>
<el-input-number v-model="reviewScore" :min="0" :max="100" :precision="0" controls-position="right" />
</el-form-item>
<el-form-item label="复核意见">
<el-input
v-model="reviewNote"
type="textarea"
:rows="4"
maxlength="1000"
show-word-limit
placeholder="记录人工判断依据,可说明是否覆盖 AI 建议"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="interviewReviewVisible = false">取消</el-button>
<el-button type="danger" :loading="interviewReviewSaving" @click="submitInterviewReview">保存复核</el-button>
</template>
</el-dialog>
<section class="panel record-panel material-review-panel">
<div class="panel-title title-with-extra">
<div>
@@ -232,6 +276,7 @@ import {
linkCandidateEmployee,
listCandidateMaterialsForReview,
listInterviewRecords,
reviewInterviewRecord,
reviewCandidateMaterial,
startInterview,
submitInterviewAnswer,
@@ -426,6 +471,11 @@ const apiQuestions = ref<InterviewQuestion[]>([]);
const apiScoreResult = ref<CandidateSeed['scoreResult'] | null>(null);
const answers = reactive<Record<string, string>>({});
const records = ref<InterviewRecord[]>([...baseRecords]);
const interviewReviewVisible = ref(false);
const interviewReviewSaving = ref(false);
const reviewingRecord = ref<InterviewRecord | null>(null);
const reviewScore = ref<number | undefined>(undefined);
const reviewNote = ref('');
const materialReviews = ref<CandidateMaterialReview[]>([]);
const materialStatus = ref('待审核');
const materialLoading = ref(false);
@@ -626,6 +676,38 @@ const loadInterviewRecords = async () => {
}
};
const openInterviewReview = (row: InterviewRecord) => {
if (!row.sessionId) {
ElMessage.warning('当前记录没有可复核的后端会话');
return;
}
reviewingRecord.value = row;
reviewScore.value = row.reviewedScore ?? row.finalScore ?? row.score;
reviewNote.value = row.reviewNote ?? '';
interviewReviewVisible.value = true;
};
const submitInterviewReview = async () => {
if (!reviewingRecord.value?.sessionId || reviewScore.value === undefined) {
ElMessage.warning('请输入人工复核分');
return;
}
interviewReviewSaving.value = true;
try {
await reviewInterviewRecord(reviewingRecord.value.sessionId, {
reviewedScore: reviewScore.value,
reviewNote: reviewNote.value.trim()
});
interviewReviewVisible.value = false;
await loadInterviewRecords();
ElMessage.success('面试结果人工复核已保存');
} catch {
ElMessage.error('面试结果人工复核失败');
} finally {
interviewReviewSaving.value = false;
}
};
const loadCandidateLink = async () => {
candidateLink.value = null;
linkExtPartyId.value = '';
@@ -1196,6 +1278,30 @@ watch(candidateId, loadCandidateLink);
width: 110px;
}
.interview-review-context {
display: grid;
gap: 6px;
margin-bottom: 18px;
padding: 12px;
border-radius: 8px;
background: #fff8ed;
color: #46566e;
strong {
color: #26354c;
}
span {
font-size: 12px;
}
}
.interview-review-form {
:deep(.el-input-number) {
width: 180px;
}
}
.material-name {
display: grid;
gap: 2px;
+4
View File
@@ -383,8 +383,12 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service
contains frontend/src/router/index.ts "/system/prompts"
contains frontend/src/api/aihr/interview.ts "/api/recruit/interview/start"
contains frontend/src/api/aihr/interview.ts "/api/recruit/interview/candidate-links"
contains frontend/src/api/aihr/interview.ts "/records/\${encodeURIComponent(sessionId)}/review"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java "/api/recruit/interview"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java "无权查看面试记录"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java "/records/{sessionId}/review"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrInterviewService.java "reviewed_score"
contains frontend/src/views/recruit/interview.vue "人工复核 AI 面试结果"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java "@SaCheckRole"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrInterviewService.java "aihr_candidate_employee_link"
contains frontend/src/api/aihr/practice.ts "/api/train/practice/start"