From ef188d3283d7d8e3e45e46f0bd882175ddbabb56 Mon Sep 17 00:00:00 2001 From: let5sne Date: Tue, 14 Jul 2026 06:22:36 +0800 Subject: [PATCH] feat(aihr): link candidates to employee principals --- .../controller/AihrInterviewController.java | 21 ++++ .../dromara/aihr/domain/AihrInterviewDto.java | 6 + .../aihr/service/AihrInterviewService.java | 84 ++++++++++++++ .../service/AihrInterviewServiceTest.java | 72 ++++++++++++ .../sql/aihr_interview_result_mysql8.sql | 15 +++ docs/API_INTEGRATION.md | 1 + docs/BRD_IMPLEMENTATION_AUDIT.md | 1 + frontend/src/api/aihr/interview.ts | 24 ++++ frontend/src/views/recruit/interview.vue | 104 +++++++++++++++++- scripts/demo-check.sh | 3 + 10 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrInterviewServiceTest.java diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java index ae88abb0..7ed3d5bb 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrInterviewController.java @@ -1,15 +1,20 @@ package org.dromara.aihr.controller; import cn.dev33.satoken.annotation.SaCheckLogin; +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.annotation.SaMode; import lombok.RequiredArgsConstructor; import org.dromara.aihr.domain.AihrInterviewDto.AnswerRequest; import org.dromara.aihr.domain.AihrInterviewDto.AnswerResponse; +import org.dromara.aihr.domain.AihrInterviewDto.CandidateLinkRequest; +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.StartRequest; import org.dromara.aihr.domain.AihrInterviewDto.StartResponse; import org.dromara.aihr.service.AihrInterviewService; +import org.dromara.common.core.constant.TenantConstants; import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.model.LoginUser; import org.dromara.common.core.enums.UserType; @@ -32,6 +37,8 @@ import java.util.List; @SaCheckLogin public class AihrInterviewController { + private static final String HR_OPERATOR_ROLE = "hr_operator"; + private final AihrInterviewService interviewService; @PostMapping("/start") @@ -56,6 +63,20 @@ 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) + @GetMapping("/candidate-links") + public R candidateLink(@RequestParam String candidateId) { + return R.ok(interviewService.candidateLink(candidateId)); + } + + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) + @PostMapping("/candidate-links") + public R linkCandidate(@RequestBody CandidateLinkRequest request) { + LoginUser loginUser = LoginHelper.getLoginUser(); + String operator = loginUser == null ? "unknown" : loginUser.getUsername(); + return R.ok(interviewService.linkCandidate(request, operator)); + } + private static StartRequest bindAppCandidate(StartRequest request) { String appUsername = currentAppUsername(); if (appUsername.isBlank()) { diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrInterviewDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrInterviewDto.java index e6c0c3a8..6d16cc99 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrInterviewDto.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrInterviewDto.java @@ -26,6 +26,12 @@ public final class AihrInterviewDto { public record FinishRequest(String sessionId, Map answers) { } + public record CandidateLinkRequest(String candidateId, String extPartyId) { + } + + public record CandidateLinkResponse(String candidateId, String extPartyId, String status, String linkedTime) { + } + public record FinishResponse( Integer totalScore, String suggestion, diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrInterviewService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrInterviewService.java index 62475111..cf4f91f7 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrInterviewService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrInterviewService.java @@ -7,6 +7,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.aihr.domain.AihrInterviewDto.AnswerRequest; import org.dromara.aihr.domain.AihrInterviewDto.AnswerResponse; +import org.dromara.aihr.domain.AihrInterviewDto.CandidateLinkRequest; +import org.dromara.aihr.domain.AihrInterviewDto.CandidateLinkResponse; import org.dromara.aihr.domain.AihrInterviewDto.DimensionResponse; import org.dromara.aihr.domain.AihrInterviewDto.FinishRequest; import org.dromara.aihr.domain.AihrInterviewDto.FinishResponse; @@ -44,6 +46,7 @@ public class AihrInterviewService { private final Map sessions = new ConcurrentHashMap<>(); private final Map candidates = buildCandidates(); private volatile boolean tableReady; + private volatile boolean candidateLinkTableReady; public StartResponse start(StartRequest request) { CandidateProfile candidate = resolveCandidate( @@ -129,6 +132,59 @@ public class AihrInterviewService { return jdbcTemplate.query(sql.toString(), this::mapRecord, args.toArray()); } + public CandidateLinkResponse candidateLink(String candidateId) { + ensureCandidateLinkTable(); + String normalizedCandidateId = clean(candidateId); + if (normalizedCandidateId.isBlank()) { + return null; + } + return jdbcTemplate.query(""" + SELECT candidate_id, ext_party_id, status, linked_time + FROM aihr_candidate_employee_link + WHERE tenant_id = '000000' AND candidate_id = ? + LIMIT 1 + """, (rs, rowNum) -> new CandidateLinkResponse( + rs.getString("candidate_id"), + rs.getString("ext_party_id"), + rs.getString("status"), + rs.getTimestamp("linked_time") == null ? "" : rs.getTimestamp("linked_time").toLocalDateTime().format(TIME_FORMAT) + ), normalizedCandidateId).stream().findFirst().orElse(null); + } + + public CandidateLinkResponse linkCandidate(CandidateLinkRequest request, String operator) { + ensureCandidateLinkTable(); + String candidateId = clean(request == null ? null : request.candidateId()); + String extPartyId = clean(request == null ? null : request.extPartyId()); + if (candidateId.isBlank() || candidateId.length() > 80) { + throw new IllegalArgumentException("候选人ID不能为空且不能超过80个字符"); + } + if (extPartyId.isBlank() || extPartyId.length() > 100) { + throw new IllegalArgumentException("员工主体ID不能为空且不能超过100个字符"); + } + Integer activeEmployee = jdbcTemplate.queryForObject(""" + SELECT COUNT(*) + FROM aihr_org_snapshot + WHERE tenant_id = '000000' + AND employment_status = 'active' + AND ext_party_id = ? + """, Integer.class, extPartyId); + if (activeEmployee == null || activeEmployee == 0) { + throw new IllegalArgumentException("员工主体不存在或已离职,请先同步在职组织快照"); + } + CandidateLinkResponse current = candidateLink(candidateId); + if (current != null && !extPartyId.equals(current.extPartyId())) { + throw new IllegalArgumentException("候选人已关联其他员工主体,请先由HR核对后处理"); + } + jdbcTemplate.update(""" + INSERT INTO aihr_candidate_employee_link + (tenant_id, candidate_id, ext_party_id, status, linked_by, linked_time, create_time, update_time) + VALUES ('000000', ?, ?, '已关联', ?, now(), now(), now()) + ON DUPLICATE KEY UPDATE + status = '已关联', linked_by = VALUES(linked_by), linked_time = now(), update_time = now() + """, candidateId, extPartyId, firstNonBlank(operator, "unknown")); + return candidateLink(candidateId); + } + private RecordResponse mapRecord(ResultSet rs, int rowNum) throws SQLException { var finishedTime = rs.getTimestamp("finished_time"); return new RecordResponse( @@ -226,6 +282,34 @@ public class AihrInterviewService { } } + private void ensureCandidateLinkTable() { + if (candidateLinkTableReady) { + return; + } + synchronized (this) { + if (candidateLinkTableReady) { + return; + } + jdbcTemplate.execute(""" + CREATE TABLE IF NOT EXISTS `aihr_candidate_employee_link` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `candidate_id` varchar(80) NOT NULL COMMENT '本地候选人ID', + `ext_party_id` varchar(100) NOT NULL COMMENT '外部员工主体ID', + `status` varchar(20) DEFAULT '已关联' COMMENT '关联状态', + `linked_by` varchar(100) DEFAULT NULL COMMENT '关联操作人', + `linked_time` datetime DEFAULT NULL COMMENT '关联时间', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_aihr_candidate_employee_link_candidate` (`tenant_id`, `candidate_id`), + KEY `idx_aihr_candidate_employee_link_party` (`tenant_id`, `ext_party_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 候选人与员工主体关联'; + """); + candidateLinkTableReady = true; + } + } + private List questionRecords(InterviewSession session) { return session.questions().stream().map(question -> { String answer = clean(session.answers().get(question.questionId())); diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrInterviewServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrInterviewServiceTest.java new file mode 100644 index 00000000..c1192ee7 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrInterviewServiceTest.java @@ -0,0 +1,72 @@ +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.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.JdbcTemplate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class AihrInterviewServiceTest { + + @Test + @Tag("dev") + void linkCandidateRejectsMissingOrInactiveEmployee() { + JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); + AihrInterviewService service = new AihrInterviewService(new ObjectMapper(), null, jdbcTemplate); + when(jdbcTemplate.queryForObject(anyString(), eq(Integer.class), eq("employee-inactive"))).thenReturn(0); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> service.linkCandidate( + new CandidateLinkRequest("candidate-1", "employee-inactive"), "hr" + )); + + assertEquals("员工主体不存在或已离职,请先同步在职组织快照", error.getMessage()); + verify(jdbcTemplate, never()).update(anyString(), eq("candidate-1"), eq("employee-inactive"), eq("hr")); + } + + @Test + @Tag("dev") + void linkCandidateRejectsChangingAnExistingEmployee() { + JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); + AihrInterviewService service = spy(new AihrInterviewService(new ObjectMapper(), null, jdbcTemplate)); + CandidateLinkResponse current = new CandidateLinkResponse("candidate-2", "employee-old", "已关联", "07-14 06:00"); + doReturn(current).when(service).candidateLink("candidate-2"); + when(jdbcTemplate.queryForObject(anyString(), eq(Integer.class), eq("employee-new"))).thenReturn(1); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> service.linkCandidate( + new CandidateLinkRequest("candidate-2", "employee-new"), "hr" + )); + + assertEquals("候选人已关联其他员工主体,请先由HR核对后处理", error.getMessage()); + verify(jdbcTemplate, never()).update(anyString(), eq("candidate-2"), eq("employee-new"), eq("hr")); + } + + @Test + @Tag("dev") + void linkCandidateIsIdempotentForTheSameEmployee() { + JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); + AihrInterviewService service = spy(new AihrInterviewService(new ObjectMapper(), null, jdbcTemplate)); + CandidateLinkResponse linked = new CandidateLinkResponse("candidate-3", "employee-3", "已关联", "07-14 06:10"); + doReturn(linked).when(service).candidateLink("candidate-3"); + when(jdbcTemplate.queryForObject(anyString(), eq(Integer.class), eq("employee-3"))).thenReturn(1); + + CandidateLinkResponse result = service.linkCandidate( + new CandidateLinkRequest("candidate-3", "employee-3"), "hr" + ); + + assertSame(linked, result); + verify(jdbcTemplate).update(anyString(), eq("candidate-3"), eq("employee-3"), eq("hr")); + } +} diff --git a/backend/script/sql/aihr_interview_result_mysql8.sql b/backend/script/sql/aihr_interview_result_mysql8.sql index 9d4cf0d8..6d905c06 100644 --- a/backend/script/sql/aihr_interview_result_mysql8.sql +++ b/backend/script/sql/aihr_interview_result_mysql8.sql @@ -25,3 +25,18 @@ CREATE TABLE IF NOT EXISTS `aihr_interview_result` ( KEY `idx_aihr_interview_candidate` (`tenant_id`, `candidate_id`, `finished_time`), KEY `idx_aihr_interview_finished` (`tenant_id`, `finished_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 面试结果'; + +CREATE TABLE IF NOT EXISTS `aihr_candidate_employee_link` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号', + `candidate_id` varchar(80) NOT NULL COMMENT '本地候选人ID', + `ext_party_id` varchar(100) NOT NULL COMMENT '外部员工主体ID', + `status` varchar(20) DEFAULT '已关联' COMMENT '关联状态', + `linked_by` varchar(100) DEFAULT NULL COMMENT '关联操作人', + `linked_time` datetime DEFAULT NULL COMMENT '关联时间', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_aihr_candidate_employee_link_candidate` (`tenant_id`, `candidate_id`), + KEY `idx_aihr_candidate_employee_link_party` (`tenant_id`, `ext_party_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 候选人与员工主体关联'; diff --git a/docs/API_INTEGRATION.md b/docs/API_INTEGRATION.md index 6792a810..8b4331f2 100644 --- a/docs/API_INTEGRATION.md +++ b/docs/API_INTEGRATION.md @@ -7,6 +7,7 @@ | 页面 | 后端接口 | 处理 | |---|---|---| | AI面试 `/recruit/interview` | `POST /api/recruit/interview/start`、`/answer`、`/finish` | 已接入真实模型优先链路:配置 chat 模型后 `/start` 动态生成面试题,`/finish` 按前端提交的真实回答做结构化评分;未配置模型时使用本地 Rubric 估分,不返回固定候选人分数 | +| 候选人入职主体关联 `/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/开发身份 | | 对练语音 | `POST /api/ai/asr`(multipart 字段 `file`,≤5MB)、`POST /api/ai/tts`(JSON `{text≤300字, voice}`,返回 `{audioUrl}` base64 dataURL)、`GET /api/aihr/mobile/oss/{ossId}` | 走 OpenAI-compatible audio 接口(如硅基流动 SenseVoice/CosyVoice2);模型管理需启用 `category=asr/tts` 配置;移动端优先用浏览器录音,`getUserMedia/MediaRecorder` 不可用或麦克风权限失败时,用 `audio/*` file input 选择/录制音频后继续调同一 ASR 接口;ASR/TTS 未配置或失败返回 fail,前端降级文本;音频下载按员工本人或主管项目范围授权,系统管理端保持后台访问 | diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 97e5f9de..b6b7346b 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -83,6 +83,7 @@ - 2026-07-14 权限审查:候选人面试、案例处理、ASR/TTS、SOP 搜索/反馈/总结卡、候选人资料接口均补控制器级登录门禁;匿名首页与 `position-sop` 导航摘要仍保持公开只读,写入、模型调用和候选人私有记录不再依赖业务层“失败返回”兜底。 - 2026-07-14 案例详情审查:管理端案例详情已同步展示 `mediaUrl` 原始音频播放器,与员工端音频回放保持一致;当前仍没有正式预渲染视频样片、学习点、主管点评和相关训练场景字段,因此不把音频回放计为完整视频学习闭环。 - 2026-07-14 BRD 深度复核:重新按“业务条款→源码路径→数据证据”核对岗位-SOP、训练/错题本、案例详情、RAG、候选人面试、组织权限和成长证据,未发现新的高置信“文档已承诺但代码完全未接通”缺口;`mobile-uni` H5 构建、类型检查与 49 个用户侧单测通过,AIHR 案例/对练定向测试 31 个通过。剩余 P0/P1 仍是正式组织映射、住宅项目试点数据、方言供应商与样本、预渲染视频资产、HR 规则契约等外部输入,不能用继续扩页面或 seed 数据替代。 +- 2026-07-14 候选人入职链路补齐:管理端候选人面试页新增 HR/管理员将本地候选人 ID 关联至已同步在职 `ext_party_id` 的入口,后端以独立关系表保存租户、主体 ID、状态和操作审计,不复制组织姓名/部门;关联目标必须存在于在职组织快照,重复同主体幂等,关联其他主体拒绝。正式组织同步和真实入职数据仍需试点验证。 - 2026-07-14 线上资源只读核对:生产管理端当前加载 `/assets/index-CVcvQ1KE.js`,可检出提示词模板标记但检不出本分支新增的 `case-audio` 管理端播放器标记;因此 `6292e741` 的管理端案例音频回放尚未发布,不能把本地源码状态当作线上状态。发布前需按现有备份、资源哈希和浏览器回归流程执行。 - 2026-07-14 发布差异取证:当前分支生产构建管理端主 JS `frontend/dist/assets/index-CJZ3Ax3Z.js` SHA-256 为 `8b9278a26ccb760abce489b12a20b221748c64d4c4153613d2511777bb9677bc`,后端 `backend/ruoyi-admin/target/ruoyi-admin.jar` 为 `2817e0b30a4a0bc52cd5d88a8dd68c52981698185fb5320c24fa2743ab17ccf7`;生产仍加载 `index-CVcvQ1KE.js`,远端 jar SHA-256 为 `18f4643f401aafeab3ee896d7d6341736d5d7ee2343fee0fa86992c5797adcb9`。本轮只读核对,未执行远端写入。 - 2026-07-14 06:07 发布完成:备份目录为 `/opt/wygj/backups/release-20260714060705`;管理端、`mobile-uni` H5 和后端 jar 均已同步,线上管理端加载 `index-CJZ3Ax3Z.js`,H5 加载 `index-D4-NrEpb.js`,H5 主 JS SHA-256 为 `ab15bfd17cabe58e2f34b0ac61ee198a837128ed85dc6bb0ddf77bc3e22dcd9c`,后端 jar SHA-256 为 `2817e0b30a4a0bc52cd5d88a8dd68c52981698185fb5320c24fa2743ab17ccf7`;`wygj-aihr.service` active,生产根站点、`/h5/`、`/prod-api/auth/tenant/list` 和员工首页 API 均返回 200,线上管理端资源可检出 `case-audio`。重启瞬间的短暂 502 在服务启动完成后恢复,未形成持续故障;Chrome headless 已确认管理端标题、H5 标题和员工首页真实渲染。 diff --git a/frontend/src/api/aihr/interview.ts b/frontend/src/api/aihr/interview.ts index 75942f64..145cf53d 100644 --- a/frontend/src/api/aihr/interview.ts +++ b/frontend/src/api/aihr/interview.ts @@ -72,6 +72,13 @@ export type InterviewRecord = { summary: string; }; +export type CandidateLinkResponse = { + candidateId: string; + extPartyId: string; + status: string; + linkedTime: string; +}; + export type CandidateMaterialReview = { id: number | string; candidateId: string; @@ -119,6 +126,23 @@ export function listInterviewRecords(limit = 20): Promise> { + return request({ + url: '/api/recruit/interview/candidate-links', + method: 'get', + params: { candidateId } + }); +} + +export function linkCandidateEmployee(candidateId: string, extPartyId: string): Promise> { + return request({ + url: '/api/recruit/interview/candidate-links', + method: 'post', + data: { candidateId, extPartyId }, + headers: { repeatSubmit: false } + }); +} + export function listCandidateMaterialsForReview(status = ''): Promise> { return request({ url: '/api/aihr/hr/candidate/materials', diff --git a/frontend/src/views/recruit/interview.vue b/frontend/src/views/recruit/interview.vue index 2c76dbc6..979b30fa 100644 --- a/frontend/src/views/recruit/interview.vue +++ b/frontend/src/views/recruit/interview.vue @@ -47,6 +47,21 @@ + +
岗位画像

{{ selectedCandidate.job.description }}

@@ -207,18 +222,21 @@