Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/HrInterviewQuestionController.java
T
QiufengandClaude Opus 4.8 5e51dc3f56 SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:15 +08:00

245 lines
11 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.kaidi.oa.web;
import com.kaidi.oa.common.ApiException;
import com.kaidi.oa.common.ApiResp;
import com.kaidi.oa.common.NotFoundException;
import com.kaidi.oa.domain.HrInterviewQuestion;
import com.kaidi.oa.repository.HrInterviewQuestionRepository;
import com.kaidi.oa.repository.HrRecruitJobRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
/**
* 行政·综合部——面试笔试题库(Module 11 缺口补全)。
*
* 满足需求:「面试话术、笔试题库由 AI 按岗位自动生成,辅助 HR 完成面试提问」。
* 弥补缺口:「无面试话术/笔试题库AI生成功能」。
*
* 本控制器提供:
* - 题库 CRUD(人工录入)
* - /ai-generate:按岗位 AI 模拟生成一组题目(关键词结构化生成,真实 LLM 接入见 AiController 扩展)
* - /by-job/{jobId}:某岗位的全部题库(面试时快速调用)
*
* AI 生成:根据岗位名、要求关键词生成 5 大类各 2 题,结构化落库后可人工编辑。
* 真实 LLM 接入时,只需将 buildAiQuestions() 替换为 AiController HTTP 调用。
*/
@RestController
@RequestMapping("/api/oa/hr-interview-questions")
public class HrInterviewQuestionController {
private static final List<String> QUESTION_TYPES =
List.of("面试话术", "笔试题", "情景题", "技能考察", "综合素质");
private final HrInterviewQuestionRepository repo;
private final HrRecruitJobRepository jobRepo;
public HrInterviewQuestionController(HrInterviewQuestionRepository repo,
HrRecruitJobRepository jobRepo) {
this.repo = repo;
this.jobRepo = jobRepo;
}
// ---------- 查询 ----------
@GetMapping
public ApiResp<List<HrInterviewQuestion>> list(
@RequestParam(required = false) Long jobId,
@RequestParam(required = false) String questionType,
@RequestParam(required = false) String status) {
if (jobId != null && status != null && !status.isBlank()) {
return ApiResp.ok(repo.findByJobIdAndStatus(jobId, status));
}
if (jobId != null) {
return ApiResp.ok(repo.findByJobId(jobId));
}
if (questionType != null && !questionType.isBlank()) {
return ApiResp.ok(repo.findByQuestionType(questionType));
}
if (status != null && !status.isBlank()) {
return ApiResp.ok(repo.findByStatus(status));
}
return ApiResp.ok(repo.findAll());
}
@GetMapping("/{id}")
public ApiResp<HrInterviewQuestion> get(@PathVariable Long id) {
return ApiResp.ok(require(id));
}
@GetMapping("/by-job/{jobId}")
public ApiResp<List<HrInterviewQuestion>> byJob(@PathVariable Long jobId) {
return ApiResp.ok(repo.findByJobIdAndStatus(jobId, "启用"));
}
// ---------- CRUD ----------
public record CreateReq(
Long jobId, String position, String questionType,
String content, String referenceAnswer,
String difficulty, String creator) {}
@PostMapping
@Transactional
public ApiResp<HrInterviewQuestion> create(@RequestBody CreateReq req) {
if (req.content() == null || req.content().isBlank()) {
throw new ApiException(400, "题目内容(content) 不能为空");
}
if (req.questionType() == null || req.questionType().isBlank()) {
throw new ApiException(400, "题型(questionType) 不能为空");
}
HrInterviewQuestion q = new HrInterviewQuestion();
q.setJobId(req.jobId());
q.setPosition(req.position());
q.setQuestionType(req.questionType().trim());
q.setContent(req.content().trim());
q.setReferenceAnswer(req.referenceAnswer());
q.setAiGenerated(false);
q.setDifficulty(req.difficulty() != null ? req.difficulty() : "中级");
q.setStatus("启用");
q.setCreator(req.creator());
q.setCreatedAt(Instant.now());
return ApiResp.ok(repo.save(q));
}
public record UpdateReq(
String questionType, String content, String referenceAnswer,
String difficulty, String status) {}
@PutMapping("/{id}")
@Transactional
public ApiResp<HrInterviewQuestion> update(@PathVariable Long id, @RequestBody UpdateReq req) {
HrInterviewQuestion q = require(id);
if (req.questionType() != null && !req.questionType().isBlank()) q.setQuestionType(req.questionType().trim());
if (req.content() != null && !req.content().isBlank()) q.setContent(req.content().trim());
if (req.referenceAnswer() != null) q.setReferenceAnswer(req.referenceAnswer());
if (req.difficulty() != null && !req.difficulty().isBlank()) q.setDifficulty(req.difficulty().trim());
if (req.status() != null && !req.status().isBlank()) q.setStatus(req.status().trim());
return ApiResp.ok(repo.save(q));
}
@DeleteMapping("/{id}")
@Transactional
public ApiResp<Void> delete(@PathVariable Long id) {
require(id);
repo.deleteById(id);
return ApiResp.ok(null);
}
// ---------- AI 模拟生成 ----------
public record AiGenReq(Long jobId, String position, String requirement, String operator) {}
/**
* AI 模拟生成题库:按岗位名 + 岗位要求,生成 5 大类各 2 题,共 10 题。
* 真实 LLM 接入:将 buildAiQuestions() 替换为 AiController HTTP 调用。
* 生成后入库(aiGenerated=true),HR 可人工编辑。
*/
@PostMapping("/ai-generate")
@Transactional
public ApiResp<List<HrInterviewQuestion>> aiGenerate(@RequestBody AiGenReq req) {
String pos = req.position() != null ? req.position() : "目标岗位";
String reqDesc = req.requirement() != null ? req.requirement() : "";
String op = req.operator() != null ? req.operator() : "AI系统";
List<HrInterviewQuestion> generated = buildAiQuestions(req.jobId(), pos, reqDesc, op);
List<HrInterviewQuestion> saved = repo.saveAll(generated);
return ApiResp.ok(saved);
}
/**
* 结构化 AI 模拟题目生成(基于岗位名 + 要求关键词)。
* 每类生成 2 题,共 10 题覆盖 5 大题型。
*/
private List<HrInterviewQuestion> buildAiQuestions(Long jobId, String position,
String requirement, String operator) {
List<HrInterviewQuestion> result = new ArrayList<>();
// 面试话术 ×2
result.add(make(jobId, position, "面试话术",
"请您简单介绍一下自己,以及为什么选择" + position + "这个岗位?",
"关注求职动机是否与岗位匹配,表达是否清晰、自信。",
"初级", operator));
result.add(make(jobId, position, "面试话术",
"您在" + (requirement.isBlank() ? "过往工作" : requirement) + "方面有哪些具体经验?请举例说明。",
"考察实际工作经验与岗位要求的匹配度,重点关注STAR法则叙述。",
"中级", operator));
// 笔试题 ×2
result.add(make(jobId, position, "笔试题",
"请简述" + position + "岗位的日常核心职责(200字以内)。",
"应涵盖该岗位主要职能领域,文字表达准确清晰。",
"初级", operator));
result.add(make(jobId, position, "笔试题",
"如果同时面对多项紧急任务,您如何进行优先级排序并确保交付质量?",
"考察时间管理能力和工作方法论,期望看到优先级矩阵/轻重缓急分类。",
"中级", operator));
// 情景题 ×2
result.add(make(jobId, position, "情景题",
"假设您负责的一份重要报告在提交当天发现数据有误,您会如何处理?",
"考察危机处理、沟通能力;期望看到:立即上报→核实数据→快速修正→复盘防范。",
"中级", operator));
result.add(make(jobId, position, "情景题",
"如果您与同事对某项工作方案存在分歧,您会如何推动决策?",
"考察协作与冲突解决能力;期望看到数据支撑、充分沟通、尊重决策。",
"中级", operator));
// 技能考察 ×2
String skillKeyword = requirement.isBlank() ? "专业技能" : requirement;
result.add(make(jobId, position, "技能考察",
"请描述您在" + skillKeyword + "方面的具体技能水平和使用经验。",
"对照岗位要求评估技能匹配度,可追问具体项目或工具使用情况。",
"中级", operator));
result.add(make(jobId, position, "技能考察",
"您最近一年在专业领域有哪些学习和提升?如何保持知识更新?",
"考察学习能力和成长意愿,期望看到主动学习的具体行动。",
"高级", operator));
// 综合素质 ×2
result.add(make(jobId, position, "综合素质",
"您认为自己最大的优点和待改善的地方分别是什么?",
"考察自我认知,期望看到客观评价和改善计划,避免假谦虚。",
"初级", operator));
result.add(make(jobId, position, "综合素质",
"您的职业发展目标是什么?三年后您希望在" + position + "方向达到什么水平?",
"考察职业规划和稳定性,判断与公司发展方向的契合度。",
"高级", operator));
return result;
}
private HrInterviewQuestion make(Long jobId, String position, String qType,
String content, String refAnswer,
String difficulty, String creator) {
HrInterviewQuestion q = new HrInterviewQuestion();
q.setJobId(jobId);
q.setPosition(position);
q.setQuestionType(qType);
q.setContent(content);
q.setReferenceAnswer(refAnswer);
q.setAiGenerated(true);
q.setDifficulty(difficulty);
q.setStatus("启用");
q.setCreator(creator);
q.setCreatedAt(Instant.now());
return q;
}
private HrInterviewQuestion require(Long id) {
return repo.findById(id)
.orElseThrow(() -> new NotFoundException("面试题不存在: " + id));
}
}