恢复点(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>
245 lines
11 KiB
Java
245 lines
11 KiB
Java
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));
|
||
}
|
||
}
|