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,
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,15 +334,35 @@ 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 面试结果';
""");
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("ALTER TABLE aihr_interview_result ADD COLUMN questions_json text DEFAULT NULL COMMENT '面试问题与回答快照JSON' AFTER answers_json");
jdbcTemplate.execute(ddl);
} catch (DataAccessException error) {
if (!String.valueOf(error.getMessage()).contains("Duplicate column")) {
if (!isDuplicateColumnError(error)) {
throw error;
}
}
tableReady = true;
}
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() {
@@ -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"