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.AuditFinding; import com.kaidi.oa.domain.AuditProject; import com.kaidi.oa.domain.AuditRiskAssessment; import com.kaidi.oa.domain.RiskAssessment; import com.kaidi.oa.repository.AuditFindingRepository; import com.kaidi.oa.repository.AuditProjectRepository; import com.kaidi.oa.repository.AuditRiskAssessmentRepository; import com.kaidi.oa.repository.RiskAssessmentRepository; 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.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.time.Year; import java.util.List; /** * 风险管理有效性审计(内控部·审计监察部·模块4)。 * * 补全 PARTIAL 缺口: * 1) 风险管理有效性审计:审计人员评估风险应对措施有效性的专用端点; * 2) 发现风险管理缺陷时自动生成 AuditFinding(无效 → 联动创建问题,双向回填 findingId); * 3) 从风险库批量导入待评估风险(PATCH /import-from-risk-library); * 4) 有效性统计摘要(GET /summary)。 * * 端点: * - POST / 新建评估记录(待评估); * - PATCH /import-from-risk-library 从 RiskAssessment 风险库导入高/重大风险批量创建待评估记录; * - PUT /{id} 编辑评估内容(待评估/评估中状态); * - PATCH /{id}/assess 提交评估结论(有效/部分有效/无效),无效时自动派生 AuditFinding; * - DELETE /{id} 删除(待评估状态); * - GET / 列表(按项目 id/状态/有效性过滤); * - GET /summary 有效性统计(有效/部分有效/无效各占比)。 */ @RestController @RequestMapping("/api/oa/audit-risk-assessments") public class AuditRiskAuditController { private final AuditRiskAssessmentRepository assessRepo; private final AuditProjectRepository projectRepo; private final RiskAssessmentRepository riskRepo; private final AuditFindingRepository findingRepo; public AuditRiskAuditController(AuditRiskAssessmentRepository assessRepo, AuditProjectRepository projectRepo, RiskAssessmentRepository riskRepo, AuditFindingRepository findingRepo) { this.assessRepo = assessRepo; this.projectRepo = projectRepo; this.riskRepo = riskRepo; this.findingRepo = findingRepo; } @GetMapping public ApiResp> list( @RequestParam(required = false) Long projectId, @RequestParam(required = false) String status, @RequestParam(required = false) String effectiveness) { if (projectId != null) { return ApiResp.ok(assessRepo.findByProjectId(projectId)); } if (status != null && !status.isBlank()) { return ApiResp.ok(assessRepo.findByStatus(status)); } if (effectiveness != null && !effectiveness.isBlank()) { return ApiResp.ok(assessRepo.findByEffectiveness(effectiveness)); } return ApiResp.ok(assessRepo.findByOrderByIdDesc()); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(load(id)); } /** 有效性统计(有效/部分有效/无效各占比,按项目统计)。 */ @GetMapping("/summary") public ApiResp summary(@RequestParam(required = false) Long projectId) { List all = projectId != null ? assessRepo.findByProjectId(projectId) : assessRepo.findAll(); long effective = all.stream().filter(a -> "有效".equals(a.getEffectiveness())).count(); long partial = all.stream().filter(a -> "部分有效".equals(a.getEffectiveness())).count(); long ineffective = all.stream().filter(a -> "无效".equals(a.getEffectiveness())).count(); long assessed = all.stream().filter(a -> a.getEffectiveness() != null && !a.getEffectiveness().isBlank()).count(); long pending = all.stream().filter(a -> a.getEffectiveness() == null || a.getEffectiveness().isBlank()).count(); java.util.Map result = new java.util.LinkedHashMap<>(); result.put("total", all.size()); result.put("assessed", assessed); result.put("pending", pending); result.put("effective", effective); result.put("partial", partial); result.put("ineffective", ineffective); result.put("defectRate", assessed <= 0 ? 0.0 : Math.round((ineffective + partial) * 10000d / assessed) / 100d); return ApiResp.ok(result); } public record CreateRequest( Long projectId, Long riskId, String riskName, String riskCategory, String riskLevel, String responsibleDept, String originalMeasure, String assessor) { } /** 新建风险管理有效性评估记录(待评估)。 */ @PostMapping @Transactional public ApiResp create(@RequestBody CreateRequest req) { if (req.projectId() == null) { throw new ApiException(400, "请选择关联审计项目"); } if ((req.riskName() == null || req.riskName().isBlank()) && req.riskId() == null) { throw new ApiException(400, "请填写风险名称或选择风险库中的风险"); } AuditProject project = projectRepo.findById(req.projectId()) .orElseThrow(() -> new ApiException(400, "审计项目不存在: " + req.projectId())); AuditRiskAssessment a = new AuditRiskAssessment(); a.setAssessNo(nextAssessNo()); a.setProjectId(project.getId()); a.setProjectName(project.getName()); a.setRiskId(req.riskId()); // 如果关联风险库,从风险库补充信息。 if (req.riskId() != null) { riskRepo.findById(req.riskId()).ifPresent(r -> { a.setRiskName(r.getRiskName()); a.setRiskCategory(r.getCategory()); a.setRiskLevel(r.getLevel()); a.setResponsibleDept(r.getResponsibleDept()); a.setOriginalMeasure(r.getMeasure()); }); } // 手动覆盖优先。 if (req.riskName() != null && !req.riskName().isBlank()) a.setRiskName(req.riskName()); if (req.riskCategory() != null) a.setRiskCategory(req.riskCategory()); if (req.riskLevel() != null) a.setRiskLevel(req.riskLevel()); if (req.responsibleDept() != null) a.setResponsibleDept(req.responsibleDept()); if (req.originalMeasure() != null) a.setOriginalMeasure(req.originalMeasure()); a.setAssessor(req.assessor()); a.setStatus("待评估"); a.setCreatedAt(Instant.now()); return ApiResp.ok(assessRepo.save(a)); } public record ImportRequest(Long projectId, String assessor, String minRiskLevel) { } /** * 从风险库批量导入高/重大风险,创建待评估记录(去重:已有 riskId 相同的不重复创建)。 */ @PostMapping("/import-from-risk-library") @Transactional public ApiResp importFromRiskLibrary(@RequestBody ImportRequest req) { if (req.projectId() == null) { throw new ApiException(400, "请指定审计项目"); } AuditProject project = projectRepo.findById(req.projectId()) .orElseThrow(() -> new ApiException(400, "审计项目不存在: " + req.projectId())); String minLevel = req.minRiskLevel() == null || req.minRiskLevel().isBlank() ? "高" : req.minRiskLevel(); List risks = riskRepo.findAll(); // 过滤满足最低风险等级。 List targetLevels = levelAtLeast(minLevel); List existing = assessRepo.findByProjectId(req.projectId()); java.util.Set existingRiskIds = new java.util.HashSet<>(); for (AuditRiskAssessment e : existing) { if (e.getRiskId() != null) existingRiskIds.add(e.getRiskId()); } int created = 0; int skipped = 0; for (RiskAssessment r : risks) { if (!targetLevels.contains(r.getLevel())) { skipped++; continue; } if (existingRiskIds.contains(r.getId())) { skipped++; continue; } AuditRiskAssessment a = new AuditRiskAssessment(); a.setAssessNo(nextAssessNo()); a.setProjectId(project.getId()); a.setProjectName(project.getName()); a.setRiskId(r.getId()); a.setRiskName(r.getRiskName()); a.setRiskCategory(r.getCategory()); a.setRiskLevel(r.getLevel()); a.setResponsibleDept(r.getResponsibleDept()); a.setOriginalMeasure(r.getMeasure()); a.setAssessor(req.assessor()); a.setStatus("待评估"); a.setCreatedAt(Instant.now()); assessRepo.save(a); existingRiskIds.add(r.getId()); created++; } return ApiResp.ok(java.util.Map.of("created", created, "skipped", skipped)); } public record UpdateRequest( String riskName, String riskCategory, String riskLevel, String responsibleDept, String originalMeasure, String assessmentDetail, String recommendation, String assessor) { } @PutMapping("/{id}") public ApiResp update(@PathVariable Long id, @RequestBody UpdateRequest req) { AuditRiskAssessment a = load(id); if ("有效".equals(a.getStatus()) || "部分有效".equals(a.getStatus()) || "无效".equals(a.getStatus())) { throw new ApiException(409, "已评估完成的记录不可修改,请联系管理员"); } if (req.riskName() != null && !req.riskName().isBlank()) a.setRiskName(req.riskName()); if (req.riskCategory() != null) a.setRiskCategory(req.riskCategory()); if (req.riskLevel() != null) a.setRiskLevel(req.riskLevel()); if (req.responsibleDept() != null) a.setResponsibleDept(req.responsibleDept()); if (req.originalMeasure() != null) a.setOriginalMeasure(req.originalMeasure()); if (req.assessmentDetail() != null) a.setAssessmentDetail(req.assessmentDetail()); if (req.recommendation() != null) a.setRecommendation(req.recommendation()); if (req.assessor() != null) a.setAssessor(req.assessor()); a.setStatus("评估中"); return ApiResp.ok(assessRepo.save(a)); } public record AssessRequest( String effectiveness, String assessmentDetail, String recommendation, String assessor, Long projectId) { } /** * 提交评估结论。effectiveness=无效时,自动在关联审计项目下派生 AuditFinding。 */ @PatchMapping("/{id}/assess") @Transactional public ApiResp assess(@PathVariable Long id, @RequestBody AssessRequest req) { AuditRiskAssessment a = load(id); if (req.effectiveness() == null || req.effectiveness().isBlank()) { throw new ApiException(400, "评估结论不能为空(有效/部分有效/无效)"); } if (!List.of("有效", "部分有效", "无效").contains(req.effectiveness())) { throw new ApiException(400, "评估结论须为:有效 / 部分有效 / 无效"); } a.setEffectiveness(req.effectiveness()); if (req.assessmentDetail() != null) a.setAssessmentDetail(req.assessmentDetail()); if (req.recommendation() != null) a.setRecommendation(req.recommendation()); if (req.assessor() != null) a.setAssessor(req.assessor()); a.setAssessDate(LocalDate.now().toString()); a.setStatus(req.effectiveness()); // 无效 → 自动派生 AuditFinding。 if ("无效".equals(req.effectiveness()) && a.getFindingId() == null) { Long linkedProjectId = a.getProjectId() != null ? a.getProjectId() : (req.projectId() != null ? req.projectId() : null); if (linkedProjectId != null) { AuditFinding finding = new AuditFinding(); finding.setProjectId(linkedProjectId); projectRepo.findById(linkedProjectId).ifPresent(p -> { finding.setProjectName(p.getName()); finding.setAuditee(p.getAuditee()); }); finding.setTitle("风险管理缺陷:" + (a.getRiskName() == null ? "" : a.getRiskName())); finding.setFactBasis("风险管理有效性审计评估编号 " + a.getAssessNo() + ":" + (a.getAssessmentDetail() == null ? "应对措施无效" : a.getAssessmentDetail())); finding.setViolatedRule("风险管理制度"); finding.setSeverity(mapRiskLevelToSeverity(a.getRiskLevel())); finding.setCategory("内控缺陷"); finding.setStatus("待沟通"); finding.setCreatedAt(Instant.now()); // 生成问题编号。 String year = String.valueOf(Year.now().getValue()); long seq = findingRepo.count() + 1; finding.setFindingNo(String.format("AF-%s-%03d", year, seq)); AuditFinding saved = findingRepo.save(finding); a.setFindingId(saved.getId()); a.setFindingNo(saved.getFindingNo()); } } return ApiResp.ok(assessRepo.save(a)); } @DeleteMapping("/{id}") public ApiResp delete(@PathVariable Long id) { AuditRiskAssessment a = load(id); if (!"待评估".equals(a.getStatus())) { throw new ApiException(409, "只有待评估状态的记录可以删除"); } assessRepo.deleteById(id); return ApiResp.ok(null); } private AuditRiskAssessment load(Long id) { return assessRepo.findById(id) .orElseThrow(() -> new NotFoundException("风险审计评估记录不存在: " + id)); } private String nextAssessNo() { String year = String.valueOf(Year.now().getValue()); long seq = assessRepo.count() + 1; return String.format("ARA-%s-%03d", year, seq); } private static List levelAtLeast(String minLevel) { return switch (minLevel) { case "重大" -> List.of("重大"); case "高" -> List.of("高", "重大"); case "中" -> List.of("中", "高", "重大"); default -> List.of("低", "中", "高", "重大"); }; } private static String mapRiskLevelToSeverity(String riskLevel) { if ("重大".equals(riskLevel)) return "高"; if ("高".equals(riskLevel)) return "高"; if ("中".equals(riskLevel)) return "中"; return "低"; } }