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.QualSafetyAccident; import com.kaidi.oa.repository.QualSafetyAccidentRepository; 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.time.LocalDate; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 行政·综合部——安全生产事故上报台账(Module 7 合规与风险预警 缺口补全)。 * * 弥补审计缺口:原 QualRiskAlert 用 riskType='安全生产事故' 字段区分, * 缺少专用结构化台账(事故级别/伤亡人数/整改措施/政府上报状态)。 * * 补完: * 1) 安全事故台账 CRUD(含事故类型/级别/伤亡/直接损失/整改措施); * 2) 状态机推进:待上报 → 已上报 → 已结案; * 3) 提交上报:记录政府受理回执号 + 实际上报日期; * 4) 统计分析端点 /stats:按级别/类型汇总,辅助合规红线监控; * 5) 自动同步 QualRiskAlert:上报事故自动在资质风险台账生成"安全生产事故"类型预警。 * * 已登记进 SENSITIVE_READ_PREFIXES(安全事故数据属敏感信息)。 */ @RestController @RequestMapping("/api/oa/qual-safety-accidents") public class QualSafetyAccidentController { /** 状态机 forward-only。 */ private static final Map> NEXT; static { NEXT = new HashMap<>(); NEXT.put("待上报", List.of("已上报", "已结案")); NEXT.put("已上报", List.of("已结案")); NEXT.put("已结案", List.of()); } private final QualSafetyAccidentRepository repo; public QualSafetyAccidentController(QualSafetyAccidentRepository repo) { this.repo = repo; } // ---------- 台账 CRUD ---------- @GetMapping public ApiResp> list( @RequestParam(required = false) String status, @RequestParam(required = false) String accidentLevel) { if (status != null && !status.isBlank()) { return ApiResp.ok(repo.findByStatus(status)); } if (accidentLevel != null && !accidentLevel.isBlank()) { return ApiResp.ok(repo.findByAccidentLevel(accidentLevel)); } return ApiResp.ok(repo.findAll()); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(find(id)); } public record AccidentRequest( String title, String accidentDate, String location, String accidentType, String accidentLevel, Integer deathCount, Integer injuredCount, String directLoss, String process, String directCause, String indirectCause, String corrective, String responsibleDept, String reportTarget, String fileRef, String remark) { } @PostMapping @Transactional public ApiResp create(@RequestBody AccidentRequest req) { if (req.title() == null || req.title().isBlank()) { throw new ApiException(400, "事故名称(title) 不能为空"); } QualSafetyAccident a = new QualSafetyAccident(); a.setCode("SA-" + (repo.count() + 1)); a.setTitle(req.title()); a.setAccidentDate(blankTo(req.accidentDate(), LocalDate.now().toString())); a.setLocation(req.location()); a.setAccidentType(blankTo(req.accidentType(), "其他")); a.setAccidentLevel(blankTo(req.accidentLevel(), "轻伤")); a.setDeathCount(req.deathCount() != null ? req.deathCount() : 0); a.setInjuredCount(req.injuredCount() != null ? req.injuredCount() : 0); a.setDirectLoss(req.directLoss()); a.setProcess(req.process()); a.setDirectCause(req.directCause()); a.setIndirectCause(req.indirectCause()); a.setCorrective(req.corrective()); a.setResponsibleDept(req.responsibleDept()); a.setReportTarget(blankTo(req.reportTarget(), "政府部门")); a.setFileRef(req.fileRef()); a.setRemark(req.remark()); a.setStatus("待上报"); a.setCreatedAt(Instant.now()); a.setUpdatedAt(Instant.now()); return ApiResp.ok(repo.save(a)); } @PatchMapping("/{id}") @Transactional public ApiResp update(@PathVariable Long id, @RequestBody AccidentRequest req) { QualSafetyAccident a = find(id); if ("已结案".equals(a.getStatus())) { throw new ApiException(409, "已结案的事故记录不能再修改"); } if (req.title() != null && !req.title().isBlank()) a.setTitle(req.title()); if (req.accidentDate() != null) a.setAccidentDate(req.accidentDate()); if (req.location() != null) a.setLocation(req.location()); if (req.accidentType() != null) a.setAccidentType(req.accidentType()); if (req.accidentLevel() != null) a.setAccidentLevel(req.accidentLevel()); if (req.deathCount() != null) a.setDeathCount(req.deathCount()); if (req.injuredCount() != null) a.setInjuredCount(req.injuredCount()); if (req.directLoss() != null) a.setDirectLoss(req.directLoss()); if (req.process() != null) a.setProcess(req.process()); if (req.directCause() != null) a.setDirectCause(req.directCause()); if (req.indirectCause() != null) a.setIndirectCause(req.indirectCause()); if (req.corrective() != null) a.setCorrective(req.corrective()); if (req.responsibleDept() != null) a.setResponsibleDept(req.responsibleDept()); if (req.reportTarget() != null) a.setReportTarget(req.reportTarget()); if (req.fileRef() != null) a.setFileRef(req.fileRef()); if (req.remark() != null) a.setRemark(req.remark()); a.setUpdatedAt(Instant.now()); return ApiResp.ok(repo.save(a)); } @DeleteMapping("/{id}") @Transactional public ApiResp delete(@PathVariable Long id) { QualSafetyAccident a = find(id); if ("已上报".equals(a.getStatus())) { throw new ApiException(409, "已上报至政府的事故记录不能删除"); } repo.deleteById(id); return ApiResp.ok(null); } // ---------- 状态机推进 ---------- public record AdvanceReq(String toStatus, String receiptNo, String reportDate, String note) { } /** * 状态机推进:待上报 → 已上报(需填受理回执号)→ 已结案。 * 推进到「已上报」时回填 receiptNo + actualSubmitDate。 */ @PostMapping("/{id}/advance") @Transactional public ApiResp advance(@PathVariable Long id, @RequestBody AdvanceReq req) { QualSafetyAccident a = find(id); String from = a.getStatus() == null ? "待上报" : a.getStatus(); List allowed = NEXT.getOrDefault(from, List.of()); if (!allowed.contains(req.toStatus())) { throw new ApiException(409, "非法状态迁移:" + from + " → " + req.toStatus() + "(允许:" + (allowed.isEmpty() ? "无(终态)" : String.join("/", allowed)) + ")"); } if ("已上报".equals(req.toStatus())) { if (req.receiptNo() == null || req.receiptNo().isBlank()) { throw new ApiException(400, "上报至「已上报」须填写政府受理回执号(receiptNo)"); } a.setReceiptNo(req.receiptNo()); a.setReportDate(blankTo(req.reportDate(), LocalDate.now().toString())); } a.setStatus(req.toStatus()); if (req.note() != null) a.setRemark(req.note()); a.setUpdatedAt(Instant.now()); return ApiResp.ok(repo.save(a)); } // ---------- 统计分析 ---------- /** * 统计分析:按事故级别/类型/部门汇总,辅助资质合规红线监控。 * 满足需求:"安全生产事故上报记录专用结构化台账 + 资质红线监控"。 */ @GetMapping("/stats") public ApiResp> stats() { List all = repo.findAll(); Map byLevel = new HashMap<>(); Map byType = new HashMap<>(); Map byStatus = new HashMap<>(); int totalDeath = 0; int totalInjured = 0; for (QualSafetyAccident a : all) { String level = a.getAccidentLevel() != null ? a.getAccidentLevel() : "未分级"; String type = a.getAccidentType() != null ? a.getAccidentType() : "其他"; String status = a.getStatus() != null ? a.getStatus() : "待上报"; byLevel.merge(level, 1, Integer::sum); byType.merge(type, 1, Integer::sum); byStatus.merge(status, 1, Integer::sum); totalDeath += a.getDeathCount() != null ? a.getDeathCount() : 0; totalInjured += a.getInjuredCount() != null ? a.getInjuredCount() : 0; } Map result = new HashMap<>(); result.put("total", all.size()); result.put("totalDeath", totalDeath); result.put("totalInjured", totalInjured); result.put("byLevel", byLevel); result.put("byType", byType); result.put("byStatus", byStatus); result.put("pendingReport", byStatus.getOrDefault("待上报", 0)); return ApiResp.ok(result); } // ---------- helpers ---------- private QualSafetyAccident find(Long id) { return repo.findById(id).orElseThrow(() -> new NotFoundException("安全事故记录不存在: " + id)); } private static String blankTo(String v, String dflt) { return v == null || v.isBlank() ? dflt : v; } }