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)。
*
*
* - /api/oa/ext-audit-plans —— 外部审核计划(认证机构/客户/政府检查)CRUD + 状态流转。
* - POST /ext-audit-plans/{id}/complete —— 完成审核,记录不符合项数量并触发整改期限。
* - POST /ext-audit-plans/{id}/archive —— 归档审核报告。
* - /api/oa/compliance-evaluations —— 年度合规性评价 CRUD。
* - POST /compliance-evaluations/{id}/complete —— 完成评价,不符合项自动转入CAPA。
*
*/
@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> 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 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 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 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 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 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 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 archiveAudit(@PathVariable Long id,
@RequestBody java.util.Map 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> 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 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 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 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 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 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 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> 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 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 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 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 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));
}
}