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 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( @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 get(@PathVariable Long id) { return ApiResp.ok(require(id)); } @GetMapping("/by-job/{jobId}") public ApiResp> 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 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 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 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> 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 generated = buildAiQuestions(req.jobId(), pos, reqDesc, op); List saved = repo.saveAll(generated); return ApiResp.ok(saved); } /** * 结构化 AI 模拟题目生成(基于岗位名 + 要求关键词)。 * 每类生成 2 题,共 10 题覆盖 5 大题型。 */ private List buildAiQuestions(Long jobId, String position, String requirement, String operator) { List 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)); } }