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>
This commit is contained in:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
@@ -0,0 +1,244 @@
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));
}
}