Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/HrAiDocController.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

458 lines
23 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.HrAiDocDraft;
import com.kaidi.oa.repository.HrAiDocDraftRepository;
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.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
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.List;
/**
* 行政·综合部——AI 文档初稿生成(Module 11 缺口闭合)。
*
* 缺口:「员工入职资料整理、岗位说明、员工手册由 AI 生成初稿」(仅题库生成存在,此子功能缺失)。
*
* 本控制器提供:
* GET /hr-ai-doc — 列表(支持 docType/status 筛选)
* GET /hr-ai-doc/{id} — 详情
* POST /hr-ai-doc — 人工录入草稿
* PATCH /hr-ai-doc/{id} — 编辑/审核更新
* DELETE /hr-ai-doc/{id} — 删除草稿
* POST /hr-ai-doc/ai-generate — AI 模拟生成指定类型文档初稿
* POST /hr-ai-doc/{id}/publish — 发布(草稿→已发布)
* POST /hr-ai-doc/{id}/review — 审核通过(草稿→已审核)
*
* 文档类型:入职资料 / 岗位说明 / 员工手册 / 转正考核 / 离职说明
* AI生成:关键词结构化展开生成 Markdown 正文,真实 LLM 接入时替换 buildDraft() 为 AiController 调用。
*/
@RestController
@RequestMapping("/api/oa/hr-ai-doc")
public class HrAiDocController {
private final HrAiDocDraftRepository repo;
public HrAiDocController(HrAiDocDraftRepository repo) {
this.repo = repo;
}
// ============ 查询 ============
@GetMapping
public ApiResp<List<HrAiDocDraft>> list(
@RequestParam(required = false) String docType,
@RequestParam(required = false) String status) {
if (docType != null && !docType.isBlank() && status != null && !status.isBlank()) {
return ApiResp.ok(repo.findByDocTypeAndStatus(docType, status));
}
if (docType != null && !docType.isBlank()) {
return ApiResp.ok(repo.findByDocType(docType));
}
if (status != null && !status.isBlank()) {
return ApiResp.ok(repo.findByStatus(status));
}
return ApiResp.ok(repo.findAll());
}
@GetMapping("/{id}")
public ApiResp<HrAiDocDraft> get(@PathVariable Long id) {
return ApiResp.ok(require(id));
}
// ============ 人工录入 ============
public record CreateReq(
String docType, String relatedPosition, String relatedDept,
String title, String content, String operator) {}
@PostMapping
@Transactional
public ApiResp<HrAiDocDraft> create(@RequestBody CreateReq req) {
if (req.docType() == null || req.docType().isBlank()) {
throw new ApiException(400, "文档类型(docType) 不能为空");
}
if (req.title() == null || req.title().isBlank()) {
throw new ApiException(400, "文档标题(title) 不能为空");
}
HrAiDocDraft d = new HrAiDocDraft();
d.setDocType(req.docType().trim());
d.setRelatedPosition(req.relatedPosition());
d.setRelatedDept(req.relatedDept());
d.setTitle(req.title().trim());
d.setContent(req.content());
d.setAiGenerated(false);
d.setStatus("草稿");
d.setOperator(req.operator());
d.setCreatedAt(Instant.now());
d.setUpdatedAt(Instant.now());
return ApiResp.ok(repo.save(d));
}
// ============ 编辑 ============
public record UpdateReq(
String title, String content, String relatedPosition,
String relatedDept, String reviewNote, String operator) {}
@PatchMapping("/{id}")
@Transactional
public ApiResp<HrAiDocDraft> update(@PathVariable Long id, @RequestBody UpdateReq req) {
HrAiDocDraft d = require(id);
if (req.title() != null && !req.title().isBlank()) d.setTitle(req.title().trim());
if (req.content() != null) d.setContent(req.content());
if (req.relatedPosition() != null) d.setRelatedPosition(req.relatedPosition());
if (req.relatedDept() != null) d.setRelatedDept(req.relatedDept());
if (req.reviewNote() != null) d.setReviewNote(req.reviewNote());
if (req.operator() != null) d.setOperator(req.operator());
d.setUpdatedAt(Instant.now());
return ApiResp.ok(repo.save(d));
}
// ============ 删除 ============
@DeleteMapping("/{id}")
@Transactional
public ApiResp<Void> delete(@PathVariable Long id) {
if (!repo.existsById(id)) throw new NotFoundException("文档草稿不存在: " + id);
repo.deleteById(id);
return ApiResp.ok(null);
}
// ============ 状态推进 ============
public record ReviewReq(String reviewNote) {}
@PostMapping("/{id}/review")
@Transactional
public ApiResp<HrAiDocDraft> review(@PathVariable Long id, @RequestBody ReviewReq req) {
HrAiDocDraft d = require(id);
if (!"草稿".equals(d.getStatus())) {
throw new ApiException(400, "只有「草稿」状态的文档才能审核");
}
d.setStatus("已审核");
if (req.reviewNote() != null) d.setReviewNote(req.reviewNote());
d.setUpdatedAt(Instant.now());
return ApiResp.ok(repo.save(d));
}
@PostMapping("/{id}/publish")
@Transactional
public ApiResp<HrAiDocDraft> publish(@PathVariable Long id) {
HrAiDocDraft d = require(id);
if (!"已审核".equals(d.getStatus()) && !"草稿".equals(d.getStatus())) {
throw new ApiException(400, "文档状态不符合发布条件");
}
d.setStatus("已发布");
d.setUpdatedAt(Instant.now());
return ApiResp.ok(repo.save(d));
}
// ============ AI 模拟生成 ============
public record AiGenReq(
String docType, String relatedPosition, String relatedDept,
String background, String operator) {}
/**
* AI 模拟生成文档初稿。
* 根据 docType 展开结构化 Markdown 正文,落库后 HR 可人工修订。
* 真实 LLM 接入:将 buildDraft() 替换为 AiController HTTP 调用。
*
* 支持类型:入职资料 / 岗位说明 / 员工手册 / 转正考核 / 离职说明
*/
@PostMapping("/ai-generate")
@Transactional
public ApiResp<HrAiDocDraft> aiGenerate(@RequestBody AiGenReq req) {
String docType = req.docType() != null ? req.docType().trim() : "入职资料";
String pos = req.relatedPosition() != null ? req.relatedPosition() : "";
String dept = req.relatedDept() != null ? req.relatedDept() : "";
String bg = req.background() != null ? req.background() : "";
String op = req.operator() != null ? req.operator() : "AI系统";
String title = buildTitle(docType, pos, dept);
String content = buildDraft(docType, pos, dept, bg);
HrAiDocDraft d = new HrAiDocDraft();
d.setDocType(docType);
d.setRelatedPosition(pos.isBlank() ? null : pos);
d.setRelatedDept(dept.isBlank() ? null : dept);
d.setTitle(title);
d.setContent(content);
d.setGenContext("类型=" + docType + " 岗位=" + pos + " 部门=" + dept + " 背景=" + bg);
d.setAiGenerated(true);
d.setStatus("草稿");
d.setOperator(op);
d.setCreatedAt(Instant.now());
d.setUpdatedAt(Instant.now());
return ApiResp.ok(repo.save(d));
}
// ============ 私有方法 ============
private HrAiDocDraft require(Long id) {
return repo.findById(id)
.orElseThrow(() -> new NotFoundException("文档草稿不存在: " + id));
}
private static String buildTitle(String docType, String pos, String dept) {
return switch (docType) {
case "岗位说明" -> (pos.isBlank() ? "" : pos + " ") + "岗位说明书(AI初稿)";
case "员工手册" -> (dept.isBlank() ? "凯迪科技" : dept) + "员工手册(AI初稿)";
case "转正考核" -> (pos.isBlank() ? "" : pos + " ") + "转正考核表(AI初稿)";
case "离职说明" -> "员工离职交接说明模板(AI初稿)";
default -> (dept.isBlank() ? "" : dept + " ") + "员工入职资料包(AI初稿)";
};
}
/**
* 结构化文档正文生成,按 docType 展开不同模板。
* 使用关键词填充,真实场景对接 LLM 后效果更佳。
*/
private static String buildDraft(String docType, String pos, String dept, String background) {
return switch (docType) {
case "岗位说明" -> buildJobDescription(pos, dept, background);
case "员工手册" -> buildEmployeeHandbook(dept);
case "转正考核" -> buildProbationReview(pos);
case "离职说明" -> buildResignationGuide();
default -> buildOnboardingDoc(dept, background);
};
}
private static String buildOnboardingDoc(String dept, String background) {
String deptName = dept.isBlank() ? "凯迪科技" : dept;
return "# " + deptName + " 新员工入职资料包\n\n"
+ "> AI初稿,请HR结合实际情况修订后发布。\n\n"
+ "## 一、欢迎信\n\n"
+ "亲爱的新同事,\n\n"
+ "热烈欢迎您加入" + deptName + "大家庭!我们期待与您共同成长。\n"
+ "请仔细阅读以下入职指引,确保顺利完成入职手续。\n\n"
+ "## 二、报到须知\n\n"
+ "**报到时间**:请于入职通知书上注明的日期,9:00前到达人力资源部。\n\n"
+ "**报到地点**:公司总部行政中心(具体楼层由HR另行通知)。\n\n"
+ "**携带材料**(原件及复印件各一份):\n"
+ "- 本人身份证\n"
+ "- 最高学历证书\n"
+ "- 学位证书\n"
+ "- 上一家单位离职证明\n"
+ "- 近期一寸免冠照片 2 张\n"
+ "- 银行卡(工资卡,建议建设银行或工商银行)\n"
+ "- 社保卡(如有)\n"
+ (background.isBlank() ? "" : "- " + background + "\n")
+ "\n## 三、入职手续办理流程\n\n"
+ "1. 到HR部门提交材料,签署劳动合同\n"
+ "2. 办理门禁卡、工牌、工位安排\n"
+ "3. 开通系统账号(OA/ERP\n"
+ "4. 由部门主管安排入职培训及岗位引导\n"
+ "5. 了解考勤、薪酬、报销等制度\n\n"
+ "## 四、入职第一周安排\n\n"
+ "| 时间 | 内容 |\n"
+ "| ---- | ---- |\n"
+ "| Day 1 | 行政手续办理、系统开通、部门介绍 |\n"
+ "| Day 2 | 公司文化、规章制度宣贯 |\n"
+ "| Day 3-4 | 岗位业务学习、导师结对 |\n"
+ "| Day 5 | 工作计划提交、与主管沟通试用期目标 |\n\n"
+ "## 五、重要联系人\n\n"
+ "- HR负责人:请入职后向直属主管确认\n"
+ "- IT支持:发送邮件至 it-support@kaidi.com\n"
+ "- 行政综合:综合办公室(内线:XXXX)\n\n"
+ "## 六、试用期说明\n\n"
+ "- 试用期一般为 **3 个月**,表现优秀者可申请提前转正\n"
+ "- 试用期内须完成《转正考核表》中的目标任务\n"
+ "- 转正后签订正式劳动合同,享受全薪及完整福利\n\n"
+ "---\n*本文档由 AI 辅助生成,最终版本需由人力资源部审核确认。*\n";
}
private static String buildJobDescription(String pos, String dept, String background) {
String posName = pos.isBlank() ? "目标岗位" : pos;
String deptName = dept.isBlank() ? "凯迪科技" : dept;
return "# " + posName + " 岗位说明书\n\n"
+ "> AI初稿,请部门主管及HR共同修订确认。\n\n"
+ "## 一、基本信息\n\n"
+ "| 项目 | 内容 |\n"
+ "| ---- | ---- |\n"
+ "| 岗位名称 | " + posName + " |\n"
+ "| 所属部门 | " + deptName + " |\n"
+ "| 直接上级 | 部门主管(由实际组织架构填写) |\n"
+ "| 岗位类别 | 专业技术类 |\n"
+ "| 编制人数 | 待定 |\n\n"
+ "## 二、岗位职责\n\n"
+ (background.isBlank()
? "1. 负责本岗位核心业务的日常执行与管理\n"
+ "2. 制定并执行工作计划,确保目标按时达成\n"
+ "3. 与相关部门协调配合,推进跨部门协作事项\n"
+ "4. 收集整理业务数据,定期提交工作报告\n"
+ "5. 参与制度建设,持续优化工作流程\n"
: "1. " + background + "\n"
+ "2. 执行上级安排,按时完成目标任务\n"
+ "3. 与相关部门协调,推动项目落地\n"
+ "4. 汇报工作进度,提交阶段成果\n"
+ "5. 提出改进建议,持续优化工作方式\n")
+ "\n## 三、任职资格\n\n"
+ "**学历要求**:本科及以上学历,相关专业优先\n\n"
+ "**工作经验**3年以上相关工作经验\n\n"
+ "**技能要求**\n"
+ "- 熟练使用 Office 办公软件\n"
+ "- 良好的沟通协调能力\n"
+ "- 具备较强的逻辑分析和解决问题能力\n"
+ (background.isBlank() ? "" : "- " + background + "相关技能\n")
+ "\n**其他要求**\n"
+ "- 工作认真负责,具备良好职业素养\n"
+ "- 具备团队合作精神\n"
+ "- 有较强的学习能力和适应能力\n\n"
+ "## 四、绩效考核维度\n\n"
+ "| 维度 | 权重 | 考核方式 |\n"
+ "| ---- | ---- | ---- |\n"
+ "| 工作完成度 | 40% | KPI达成率 |\n"
+ "| 工作质量 | 30% | 主管评分 |\n"
+ "| 协作表现 | 20% | 360度反馈 |\n"
+ "| 学习发展 | 10% | 培训完成情况 |\n\n"
+ "## 五、审批\n\n"
+ "- 起草人:HR / AI辅助生成\n"
+ "- 审核人:部门主管\n"
+ "- 批准人:分管领导\n\n"
+ "---\n*本文档由 AI 辅助生成,最终版本需由部门主管及人力资源部共同确认。*\n";
}
private static String buildEmployeeHandbook(String dept) {
String company = dept.isBlank() ? "凯迪科技有限公司" : dept;
return "# " + company + " 员工手册\n\n"
+ "> AI初稿,请法务部门及人力资源部审核后正式发布。\n\n"
+ "## 第一章 公司简介\n\n"
+ company + "(以下简称「公司」)是一家专注于工程技术、ERP管理的企业,"
+ "致力于为客户提供高质量的解决方案。\n\n"
+ "## 第二章 行为规范\n\n"
+ "### 2.1 基本准则\n"
+ "- 诚实守信,不弄虚作假\n"
+ "- 尊重他人,维护良好工作氛围\n"
+ "- 保守公司机密,不泄露商业信息\n"
+ "- 遵纪守法,不从事违法违规活动\n\n"
+ "### 2.2 保密义务\n"
+ "员工在职期间及离职后2年内,对公司以下信息负有保密义务:\n"
+ "- 客户名单及合同信息\n"
+ "- 公司财务数据\n"
+ "- 技术方案及知识产权\n"
+ "- 内部管理制度\n\n"
+ "## 第三章 考勤制度\n\n"
+ "- **工作时间**:每周一至周五,09:00-18:00(午休12:00-13:00\n"
+ "- **打卡方式**:APP/指纹/人脸识别,上下班各打卡一次\n"
+ "- **迟到处理**:迟到15分钟以上计入考勤异常\n"
+ "- **加班**:需提前申请,按国家规定给予加班补偿\n\n"
+ "## 第四章 假期制度\n\n"
+ "| 假期类型 | 时长 | 审批流程 |\n"
+ "| ---- | ---- | ---- |\n"
+ "| 年假 | 5-15天(按工龄) | 部门主管审批 |\n"
+ "| 病假 | 按医院证明 | 提交病假条至HR |\n"
+ "| 婚假 | 3天 | HR审批 |\n"
+ "| 产假 | 按国家规定 | HR审批 |\n"
+ "| 陪产假 | 15天 | HR审批 |\n"
+ "| 丧假 | 3天 | HR审批 |\n\n"
+ "## 第五章 薪酬福利\n\n"
+ "- **薪资**:每月15日发放,转账至员工银行卡\n"
+ "- **社保**:按国家规定缴纳养老、医疗、失业、工伤、生育保险\n"
+ "- **住房公积金**:按当地比例缴纳\n"
+ "- **补贴**:交通补贴、餐饮补贴(具体标准见薪酬方案)\n"
+ "- **节日福利**:传统节日发放节日礼品/礼金\n\n"
+ "## 第六章 离职管理\n\n"
+ "- 试用期离职提前 **3天** 通知\n"
+ "- 正式员工离职提前 **1个月** 书面通知\n"
+ "- 完成交接方可办理离职手续,结清薪资\n\n"
+ "## 第七章 违规处理\n\n"
+ "- **口头警告**:轻微违规,初次\n"
+ "- **书面警告**:同类违规再次出现\n"
+ "- **解除合同**:严重违规(泄密/造假/违法等)\n\n"
+ "---\n*本手册由 AI 辅助生成,最终版本需法务部门及HR审核签发。*\n";
}
private static String buildProbationReview(String pos) {
String posName = pos.isBlank() ? "【岗位名称】" : pos;
return "# " + posName + " 转正考核表\n\n"
+ "> AI初稿,请直属主管填写后经HR确认归档。\n\n"
+ "## 基本信息\n\n"
+ "| 项目 | 内容 |\n"
+ "| ---- | ---- |\n"
+ "| 员工姓名 | |\n"
+ "| 岗位 | " + posName + " |\n"
+ "| 入职日期 | |\n"
+ "| 试用期截止 | |\n"
+ "| 考核日期 | |\n"
+ "| 直属主管 | |\n\n"
+ "## 试用期目标完成情况\n\n"
+ "| 目标 | 计划完成时间 | 实际完成情况 | 完成率 |\n"
+ "| ---- | ---- | ---- | ---- |\n"
+ "| 完成岗位培训 | 入职第1周 | | |\n"
+ "| 熟悉业务流程 | 入职第2周 | | |\n"
+ "| 独立完成首个任务 | 入职第1月 | | |\n"
+ "| 达成季度工作指标 | 试用期末 | | |\n\n"
+ "## 综合评分(100分)\n\n"
+ "| 评分维度 | 权重 | 得分 | 说明 |\n"
+ "| ---- | ---- | ---- | ---- |\n"
+ "| 工作能力 | 30% | | |\n"
+ "| 工作态度 | 20% | | |\n"
+ "| 学习能力 | 20% | | |\n"
+ "| 团队协作 | 15% | | |\n"
+ "| 执行力 | 15% | | |\n"
+ "| **合计** | 100% | | |\n\n"
+ "## 转正意见\n\n"
+ "- [ ] 正式转正(总分 ≥ 80\n"
+ "- [ ] 延长试用期(60 ≤ 总分 < 80\n"
+ "- [ ] 不予转正(总分 < 60 或有违规行为)\n\n"
+ "**主管评语**\n\n"
+ "**员工自评**\n\n"
+ "## 签字确认\n\n"
+ "| 角色 | 签字 | 日期 |\n"
+ "| ---- | ---- | ---- |\n"
+ "| 员工本人 | | |\n"
+ "| 直属主管 | | |\n"
+ "| 部门负责人 | | |\n"
+ "| HR负责人 | | |\n\n"
+ "---\n*本表格由 AI 辅助生成,请主管如实填写评估内容。*\n";
}
private static String buildResignationGuide() {
return "# 员工离职交接说明模板\n\n"
+ "> AI初稿,请HR根据公司实际情况修订后使用。\n\n"
+ "## 一、离职申请流程\n\n"
+ "1. 员工提交《离职申请书》(正式员工提前1个月,试用期提前3天)\n"
+ "2. 直属主管确认离职日期,发起离职审批流程\n"
+ "3. 各部门审批通过后,确认最后工作日\n\n"
+ "## 二、工作交接清单\n\n"
+ "### 文档资料交接\n"
+ "- [ ] 在制项目文档、合同、方案\n"
+ "- [ ] 客户/供应商联系信息\n"
+ "- [ ] 内部制度文件、操作手册\n"
+ "- [ ] 未归档的工作成果\n\n"
+ "### 系统账号交接\n"
+ "- [ ] OA系统账号密码(交IT部门注销)\n"
+ "- [ ] 业务系统(ERP/CRM等)账号处理\n"
+ "- [ ] 邮件转发设置(至少转发1个月)\n"
+ "- [ ] 其他外部账号(行业平台、招聘网站等)\n\n"
+ "### 物品归还\n"
+ "- [ ] 工牌/门禁卡\n"
+ "- [ ] 电脑及外设(充电器、鼠标等)\n"
+ "- [ ] 公司书籍资料\n"
+ "- [ ] 其他公司财产\n\n"
+ "## 三、财务结算\n\n"
+ "- 报销:离职前提交所有未报销凭证\n"
+ "- 工资:最后工资于离职后次月15日发放\n"
+ "- 社保/公积金:离职当月停缴,次月起不再缴纳\n"
+ "- 押金:如有押金,凭结清证明退还\n\n"
+ "## 四、离职证明\n\n"
+ "完成全部交接手续并经HR确认后,公司出具《离职证明》,一般3个工作日内完成。\n\n"
+ "## 五、保密提示\n\n"
+ "离职后,员工仍需遵守签订的《保密协议》及《竞业限制协议》(如有),"
+ "不得泄露公司商业秘密或为竞争对手提供服务。\n\n"
+ "---\n*本文档由 AI 辅助生成,请HR结合实际修订后正式使用。*\n";
}
}