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,393 @@
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.ComplianceEvaluation;
import com.kaidi.oa.domain.EhsAuditPrepTask;
import com.kaidi.oa.domain.EhsCapa;
import com.kaidi.oa.domain.ExtAuditPlan;
import com.kaidi.oa.repository.ComplianceEvaluationRepository;
import com.kaidi.oa.repository.EhsAuditPrepTaskRepository;
import com.kaidi.oa.repository.EhsCapaRepository;
import com.kaidi.oa.repository.ExtAuditPlanRepository;
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.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
/**
* 体系认证与外部审核管理(需求 §6)。
*
* <ul>
* <li>/api/oa/ext-audit-plans —— 外部审核计划(认证机构/客户/政府检查)CRUD + 状态流转。</li>
* <li>POST /ext-audit-plans/{id}/complete —— 完成审核,记录不符合项数量并触发整改期限。</li>
* <li>POST /ext-audit-plans/{id}/archive —— 归档审核报告。</li>
* <li>/api/oa/compliance-evaluations —— 年度合规性评价 CRUD。</li>
* <li>POST /compliance-evaluations/{id}/complete —— 完成评价,不符合项自动转入CAPA。</li>
* </ul>
*/
@RestController
public class ExtAuditController {
private final ExtAuditPlanRepository auditRepo;
private final ComplianceEvaluationRepository evalRepo;
private final EhsCapaRepository capaRepo;
private final EhsAuditPrepTaskRepository prepTaskRepo;
public ExtAuditController(ExtAuditPlanRepository auditRepo,
ComplianceEvaluationRepository evalRepo,
EhsCapaRepository capaRepo,
EhsAuditPrepTaskRepository prepTaskRepo) {
this.auditRepo = auditRepo;
this.evalRepo = evalRepo;
this.capaRepo = capaRepo;
this.prepTaskRepo = prepTaskRepo;
}
// ============================= 外部审核计划 =============================
@GetMapping("/api/oa/ext-audit-plans")
public ApiResp<List<ExtAuditPlan>> listAudits(
@RequestParam(required = false) String status,
@RequestParam(required = false) String auditType,
@RequestParam(required = false) String auditStandard) {
if (status != null && !status.isBlank()) return ApiResp.ok(auditRepo.findByStatus(status));
if (auditType != null && !auditType.isBlank()) return ApiResp.ok(auditRepo.findByAuditType(auditType));
if (auditStandard != null && !auditStandard.isBlank()) return ApiResp.ok(auditRepo.findByAuditStandard(auditStandard));
return ApiResp.ok(auditRepo.findAll());
}
@GetMapping("/api/oa/ext-audit-plans/{id}")
public ApiResp<ExtAuditPlan> getAudit(@PathVariable Long id) {
return ApiResp.ok(mustAudit(id));
}
public record AuditRequest(
String title, String auditType, String auditOrg, String auditStandard,
String plannedDate, String actualDate, String scope, String coordinator,
String prepTasks, Integer ncCount, Integer obsCount,
String rectifyDueDate, String rectifyDoneDate, String reportRef,
String conclusion, String status, String remark) {
}
@PostMapping("/api/oa/ext-audit-plans")
@Transactional
public ApiResp<ExtAuditPlan> createAudit(@RequestBody AuditRequest req) {
if (req.title() == null || req.title().isBlank()) {
throw new ApiException(400, "审核标题(title)不能为空");
}
ExtAuditPlan p = new ExtAuditPlan();
fillAudit(p, req);
p.setCode("EAP-" + (auditRepo.count() + 1));
if (p.getStatus() == null || p.getStatus().isBlank()) p.setStatus("计划中");
p.setCreatedAt(Instant.now());
return ApiResp.ok(auditRepo.save(p));
}
@PutMapping("/api/oa/ext-audit-plans/{id}")
@Transactional
public ApiResp<ExtAuditPlan> updateAudit(@PathVariable Long id, @RequestBody AuditRequest req) {
ExtAuditPlan p = mustAudit(id);
fillAudit(p, req);
return ApiResp.ok(auditRepo.save(p));
}
@DeleteMapping("/api/oa/ext-audit-plans/{id}")
public ApiResp<Void> deleteAudit(@PathVariable Long id) {
if (!auditRepo.existsById(id)) throw new NotFoundException("外部审核计划不存在: " + id);
auditRepo.deleteById(id);
return ApiResp.ok(null);
}
/** 启动审核(计划中 → 进行中)。 */
@PostMapping("/api/oa/ext-audit-plans/{id}/start")
@Transactional
public ApiResp<ExtAuditPlan> startAudit(@PathVariable Long id) {
ExtAuditPlan p = mustAudit(id);
if (!"计划中".equals(p.getStatus()) && !"准备中".equals(p.getStatus())) {
throw new ApiException(400, "仅计划中/准备中的审核可启动,当前状态: " + p.getStatus());
}
p.setStatus("进行中");
if (p.getActualDate() == null || p.getActualDate().isBlank()) {
p.setActualDate(LocalDate.now().toString());
}
return ApiResp.ok(auditRepo.save(p));
}
public record CompleteAuditRequest(Integer ncCount, Integer obsCount, String conclusion, String rectifyDueDate) {
}
/** 完成审核(进行中 → 已完成),记录不符合项,设置整改期限。 */
@PostMapping("/api/oa/ext-audit-plans/{id}/complete")
@Transactional
public ApiResp<ExtAuditPlan> completeAudit(@PathVariable Long id, @RequestBody CompleteAuditRequest req) {
ExtAuditPlan p = mustAudit(id);
if (!"进行中".equals(p.getStatus())) {
throw new ApiException(400, "仅进行中的审核可完成,当前状态: " + p.getStatus());
}
if (req.ncCount() != null) p.setNcCount(req.ncCount());
if (req.obsCount() != null) p.setObsCount(req.obsCount());
if (req.conclusion() != null && !req.conclusion().isBlank()) p.setConclusion(req.conclusion());
if (req.rectifyDueDate() != null) p.setRectifyDueDate(req.rectifyDueDate());
p.setStatus("已完成");
return ApiResp.ok(auditRepo.save(p));
}
/** 归档审核报告(已完成 → 已归档)。 */
@PostMapping("/api/oa/ext-audit-plans/{id}/archive")
@Transactional
public ApiResp<ExtAuditPlan> archiveAudit(@PathVariable Long id,
@RequestBody java.util.Map<String, String> body) {
ExtAuditPlan p = mustAudit(id);
if (!"已完成".equals(p.getStatus())) {
throw new ApiException(400, "仅已完成的审核可归档,当前状态: " + p.getStatus());
}
String reportRef = body.get("reportRef");
if (reportRef != null && !reportRef.isBlank()) p.setReportRef(reportRef);
String rectifyDoneDate = body.get("rectifyDoneDate");
if (rectifyDoneDate != null && !rectifyDoneDate.isBlank()) p.setRectifyDoneDate(rectifyDoneDate);
p.setStatus("已归档");
return ApiResp.ok(auditRepo.save(p));
}
// ============================= 合规性评价 =============================
@GetMapping("/api/oa/compliance-evaluations")
public ApiResp<List<ComplianceEvaluation>> listEvals(
@RequestParam(required = false) String evalYear,
@RequestParam(required = false) String status,
@RequestParam(required = false) String evalScope) {
if (evalYear != null && !evalYear.isBlank()) return ApiResp.ok(evalRepo.findByEvalYear(evalYear));
if (status != null && !status.isBlank()) return ApiResp.ok(evalRepo.findByStatus(status));
if (evalScope != null && !evalScope.isBlank()) return ApiResp.ok(evalRepo.findByEvalScope(evalScope));
return ApiResp.ok(evalRepo.findAll());
}
@GetMapping("/api/oa/compliance-evaluations/{id}")
public ApiResp<ComplianceEvaluation> getEval(@PathVariable Long id) {
return ApiResp.ok(mustEval(id));
}
public record EvalRequest(
String evalYear, String evalScope, String plannedDate, String actualDate,
String evaluator, String evalContent, Integer totalItems, Integer compliantItems,
Integer ncItems, String ncDescription, String evalResult, String capaRefs,
String improvementSuggestions, String reportRef, String status, String remark) {
}
@PostMapping("/api/oa/compliance-evaluations")
@Transactional
public ApiResp<ComplianceEvaluation> createEval(@RequestBody EvalRequest req) {
if (req.evalYear() == null || req.evalYear().isBlank()) {
throw new ApiException(400, "评价年度(evalYear)不能为空");
}
ComplianceEvaluation e = new ComplianceEvaluation();
fillEval(e, req);
e.setCode("CE-" + (evalRepo.count() + 1));
if (e.getStatus() == null || e.getStatus().isBlank()) e.setStatus("计划");
e.setCreatedAt(Instant.now());
return ApiResp.ok(evalRepo.save(e));
}
@PutMapping("/api/oa/compliance-evaluations/{id}")
@Transactional
public ApiResp<ComplianceEvaluation> updateEval(@PathVariable Long id, @RequestBody EvalRequest req) {
ComplianceEvaluation e = mustEval(id);
fillEval(e, req);
return ApiResp.ok(evalRepo.save(e));
}
@DeleteMapping("/api/oa/compliance-evaluations/{id}")
public ApiResp<Void> deleteEval(@PathVariable Long id) {
if (!evalRepo.existsById(id)) throw new NotFoundException("合规性评价不存在: " + id);
evalRepo.deleteById(id);
return ApiResp.ok(null);
}
public record CompleteEvalRequest(
Integer totalItems, Integer compliantItems, Integer ncItems,
String ncDescription, String evalResult, String improvementSuggestions, String reportRef) {
}
/**
* 完成合规性评价,不符合项自动转入 CAPA(需求:不符合项自动转CAPA)。
*/
@PostMapping("/api/oa/compliance-evaluations/{id}/complete")
@Transactional
public ApiResp<ComplianceEvaluation> completeEval(@PathVariable Long id, @RequestBody CompleteEvalRequest req) {
ComplianceEvaluation e = mustEval(id);
if ("已完成".equals(e.getStatus()) || "CAPA跟踪中".equals(e.getStatus())) {
throw new ApiException(400, "评价已完成,不可重复提交");
}
if (req.totalItems() != null) e.setTotalItems(req.totalItems());
if (req.compliantItems() != null) e.setCompliantItems(req.compliantItems());
if (req.ncItems() != null) e.setNcItems(req.ncItems());
if (req.ncDescription() != null) e.setNcDescription(req.ncDescription());
if (req.evalResult() != null && !req.evalResult().isBlank()) e.setEvalResult(req.evalResult());
if (req.improvementSuggestions() != null) e.setImprovementSuggestions(req.improvementSuggestions());
if (req.reportRef() != null) e.setReportRef(req.reportRef());
e.setActualDate(LocalDate.now().toString());
// 自动为每个不符合项创建 CAPA
int ncCount = req.ncItems() == null ? 0 : req.ncItems();
List<String> capaRefList = new ArrayList<>();
if (ncCount > 0 && req.ncDescription() != null && !req.ncDescription().isBlank()) {
String[] ncItems = req.ncDescription().split("[,\n]");
for (String nc : ncItems) {
if (nc.isBlank()) continue;
EhsCapa capa = new EhsCapa();
capa.setCode("CAPA-CE-" + e.getCode() + "-" + (capaRepo.count() + 1));
capa.setSourceType("合规性评价");
capa.setTitle("合规性评价不符合项: " + nc.trim());
capa.setProblem(nc.trim());
capa.setProblemType("合规");
capa.setStatus("待分析");
capa.setCreatedAt(Instant.now());
EhsCapa saved = capaRepo.save(capa);
capaRefList.add(saved.getCode());
}
}
if (!capaRefList.isEmpty()) {
e.setCapaRefs(String.join(",", capaRefList));
e.setStatus("CAPA跟踪中");
} else {
e.setStatus("已完成");
}
return ApiResp.ok(evalRepo.save(e));
}
// ============================= 准备任务分配(需求:审核前准备任务分配)=============================
public record PrepTaskRequest(
String taskTitle, String description, String assignee,
String dueDate, String taskStatus, String priority, String remark) {
}
/** 获取指定审核计划的所有准备任务 */
@GetMapping("/api/oa/ext-audit-plans/{id}/prep-tasks")
public ApiResp<List<EhsAuditPrepTask>> listPrepTasks(@PathVariable Long id) {
if (!auditRepo.existsById(id)) throw new NotFoundException("外部审核计划不存在: " + id);
return ApiResp.ok(prepTaskRepo.findByAuditPlanId(id));
}
/** 为审核计划新增一条准备任务 */
@PostMapping("/api/oa/ext-audit-plans/{id}/prep-tasks")
@Transactional
public ApiResp<EhsAuditPrepTask> addPrepTask(@PathVariable Long id,
@RequestBody PrepTaskRequest req) {
if (!auditRepo.existsById(id)) throw new NotFoundException("外部审核计划不存在: " + id);
if (req.taskTitle() == null || req.taskTitle().isBlank()) {
throw new ApiException(400, "任务标题(taskTitle)不能为空");
}
EhsAuditPrepTask t = new EhsAuditPrepTask();
t.setAuditPlanId(id);
t.setTaskTitle(req.taskTitle());
t.setDescription(req.description());
t.setAssignee(req.assignee());
t.setDueDate(req.dueDate());
t.setTaskStatus(req.taskStatus() == null || req.taskStatus().isBlank() ? "待办" : req.taskStatus());
t.setPriority(req.priority() == null || req.priority().isBlank() ? "" : req.priority());
t.setRemark(req.remark());
t.setCreatedAt(Instant.now());
return ApiResp.ok(prepTaskRepo.save(t));
}
/** 更新准备任务 */
@PutMapping("/api/oa/ext-audit-plan-tasks/{taskId}")
@Transactional
public ApiResp<EhsAuditPrepTask> updatePrepTask(@PathVariable Long taskId,
@RequestBody PrepTaskRequest req) {
EhsAuditPrepTask t = prepTaskRepo.findById(taskId)
.orElseThrow(() -> new NotFoundException("准备任务不存在: " + taskId));
if (req.taskTitle() != null && !req.taskTitle().isBlank()) t.setTaskTitle(req.taskTitle());
if (req.description() != null) t.setDescription(req.description());
if (req.assignee() != null) t.setAssignee(req.assignee());
if (req.dueDate() != null) t.setDueDate(req.dueDate());
if (req.taskStatus() != null && !req.taskStatus().isBlank()) t.setTaskStatus(req.taskStatus());
if (req.priority() != null && !req.priority().isBlank()) t.setPriority(req.priority());
if (req.remark() != null) t.setRemark(req.remark());
return ApiResp.ok(prepTaskRepo.save(t));
}
/** 完成准备任务(状态置已完成,记录完成日期) */
@PostMapping("/api/oa/ext-audit-plan-tasks/{taskId}/done")
@Transactional
public ApiResp<EhsAuditPrepTask> donePrepTask(@PathVariable Long taskId) {
EhsAuditPrepTask t = prepTaskRepo.findById(taskId)
.orElseThrow(() -> new NotFoundException("准备任务不存在: " + taskId));
t.setTaskStatus("已完成");
t.setDoneDate(LocalDate.now().toString());
return ApiResp.ok(prepTaskRepo.save(t));
}
/** 删除准备任务 */
@DeleteMapping("/api/oa/ext-audit-plan-tasks/{taskId}")
public ApiResp<Void> deletePrepTask(@PathVariable Long taskId) {
if (!prepTaskRepo.existsById(taskId)) throw new NotFoundException("准备任务不存在: " + taskId);
prepTaskRepo.deleteById(taskId);
return ApiResp.ok(null);
}
// ============================= helpers =============================
private void fillAudit(ExtAuditPlan p, AuditRequest req) {
if (req.title() != null && !req.title().isBlank()) p.setTitle(req.title());
if (req.auditType() != null) p.setAuditType(req.auditType());
if (req.auditOrg() != null) p.setAuditOrg(req.auditOrg());
if (req.auditStandard() != null) p.setAuditStandard(req.auditStandard());
if (req.plannedDate() != null) p.setPlannedDate(req.plannedDate());
if (req.actualDate() != null) p.setActualDate(req.actualDate());
if (req.scope() != null) p.setScope(req.scope());
if (req.coordinator() != null) p.setCoordinator(req.coordinator());
if (req.prepTasks() != null) p.setPrepTasks(req.prepTasks());
if (req.ncCount() != null) p.setNcCount(req.ncCount());
if (req.obsCount() != null) p.setObsCount(req.obsCount());
if (req.rectifyDueDate() != null) p.setRectifyDueDate(req.rectifyDueDate());
if (req.rectifyDoneDate() != null) p.setRectifyDoneDate(req.rectifyDoneDate());
if (req.reportRef() != null) p.setReportRef(req.reportRef());
if (req.conclusion() != null) p.setConclusion(req.conclusion());
if (req.status() != null && !req.status().isBlank()) p.setStatus(req.status());
if (req.remark() != null) p.setRemark(req.remark());
}
private void fillEval(ComplianceEvaluation e, EvalRequest req) {
if (req.evalYear() != null && !req.evalYear().isBlank()) e.setEvalYear(req.evalYear());
if (req.evalScope() != null) e.setEvalScope(req.evalScope());
if (req.plannedDate() != null) e.setPlannedDate(req.plannedDate());
if (req.actualDate() != null) e.setActualDate(req.actualDate());
if (req.evaluator() != null) e.setEvaluator(req.evaluator());
if (req.evalContent() != null) e.setEvalContent(req.evalContent());
if (req.totalItems() != null) e.setTotalItems(req.totalItems());
if (req.compliantItems() != null) e.setCompliantItems(req.compliantItems());
if (req.ncItems() != null) e.setNcItems(req.ncItems());
if (req.ncDescription() != null) e.setNcDescription(req.ncDescription());
if (req.evalResult() != null) e.setEvalResult(req.evalResult());
if (req.capaRefs() != null) e.setCapaRefs(req.capaRefs());
if (req.improvementSuggestions() != null) e.setImprovementSuggestions(req.improvementSuggestions());
if (req.reportRef() != null) e.setReportRef(req.reportRef());
if (req.status() != null && !req.status().isBlank()) e.setStatus(req.status());
if (req.remark() != null) e.setRemark(req.remark());
}
private ExtAuditPlan mustAudit(Long id) {
return auditRepo.findById(id)
.orElseThrow(() -> new NotFoundException("外部审核计划不存在: " + id));
}
private ComplianceEvaluation mustEval(Long id) {
return evalRepo.findById(id)
.orElseThrow(() -> new NotFoundException("合规性评价不存在: " + id));
}
}