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.LabIncident; import com.kaidi.oa.domain.LabSafetyCheck; import com.kaidi.oa.domain.LabWaste; import com.kaidi.oa.repository.LabIncidentRepository; import com.kaidi.oa.repository.LabSafetyCheckRepository; import com.kaidi.oa.repository.LabWasteRepository; import org.springframework.transaction.annotation.Transactional; 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.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.List; /** * 实验室·安全与环境 EHS(§8 深化)。三块实验室专项能力,区别于运营/质安部通用域: * * 1) 废弃物管理:实验室废弃物(含生物废物)产生 → 转移 → 处置,暂存位/环保台账对接(sync-epb); * 2) 安全巡检:灭火器/洗眼器/通风橱 项化 checklist,按周期 plan 自动生成巡检计划 → 逐项打勾 → 异常触发整改; * 3) 事件上报:实验室微小事故(化学品泄漏/灼伤)在线上报 → 处理 → 整改 → 关闭闭环。 * * 写口默认受 default-deny(ADMIN/APPROVER) 保护。 */ @RestController @RequestMapping("/api/oa/lab-ehs") public class LabEhsController { /** 实验室安全巡检标准检查项(项化 checklist)。 */ private static final List CHECK_ITEMS = List.of( "灭火器", "洗眼器", "通风橱", "气瓶固定", "危化品柜双锁", "应急喷淋", "急救箱"); private static final List WASTE_CATEGORIES = List.of( "废液", "固废", "生物废物", "废气吸收液", "实验动物尸体"); private final LabWasteRepository wasteRepo; private final LabSafetyCheckRepository checkRepo; private final LabIncidentRepository incidentRepo; public LabEhsController(LabWasteRepository wasteRepo, LabSafetyCheckRepository checkRepo, LabIncidentRepository incidentRepo) { this.wasteRepo = wasteRepo; this.checkRepo = checkRepo; this.incidentRepo = incidentRepo; } // ==================== 废弃物管理 ==================== @GetMapping("/wastes") public ApiResp> wastes(@RequestParam(required = false) String status, @RequestParam(required = false) String category) { if (status != null && !status.isBlank()) { return ApiResp.ok(wasteRepo.findByStatus(status)); } if (category != null && !category.isBlank()) { return ApiResp.ok(wasteRepo.findByCategory(category)); } return ApiResp.ok(wasteRepo.findAll()); } public record WasteRequest(String name, String category, String quantity, String source, String storageLocation, String generatedDate, String custodian) { } /** 登记废弃物产生(落「暂存」)。 */ @PostMapping("/wastes") @Transactional public ApiResp createWaste(@RequestBody WasteRequest req) { if (req.name() == null || req.name().isBlank()) { throw new ApiException(400, "废弃物名称(name) 不能为空"); } if (req.category() != null && !req.category().isBlank() && !WASTE_CATEGORIES.contains(req.category())) { throw new ApiException(400, "分类只能是 " + WASTE_CATEGORIES); } LabWaste w = new LabWaste(); w.setWasteNo("LW-" + (wasteRepo.count() + 1)); w.setName(req.name()); w.setCategory(req.category()); w.setQuantity(req.quantity()); w.setSource(req.source()); w.setStorageLocation(req.storageLocation()); w.setGeneratedDate(req.generatedDate() == null || req.generatedDate().isBlank() ? LocalDate.now().toString() : req.generatedDate()); w.setCustodian(req.custodian()); w.setStatus("暂存"); w.setSyncEpb(false); w.setCreatedAt(Instant.now()); return ApiResp.ok(wasteRepo.save(w)); } public record TransferRequest(String transferTo, String transferDate) { } /** 转移:暂存 → 转移。 */ @PostMapping("/wastes/{id}/transfer") @Transactional public ApiResp transferWaste(@PathVariable Long id, @RequestBody TransferRequest req) { LabWaste w = mustWaste(id); if (!"暂存".equals(w.getStatus())) { throw new ApiException(409, "仅暂存态可转移,当前:" + w.getStatus()); } if (req.transferTo() == null || req.transferTo().isBlank()) { throw new ApiException(400, "承运方/转移单号(transferTo) 不能为空"); } w.setTransferTo(req.transferTo()); w.setTransferDate(req.transferDate() == null || req.transferDate().isBlank() ? LocalDate.now().toString() : req.transferDate()); w.setStatus("转移"); return ApiResp.ok(wasteRepo.save(w)); } public record DisposeWasteRequest(String disposalMethod, String disposedDate) { } /** 处置:转移 → 已处置。 */ @PostMapping("/wastes/{id}/dispose") @Transactional public ApiResp disposeWaste(@PathVariable Long id, @RequestBody DisposeWasteRequest req) { LabWaste w = mustWaste(id); if (!"转移".equals(w.getStatus()) && !"暂存".equals(w.getStatus())) { throw new ApiException(409, "当前状态不可处置:" + w.getStatus()); } if (req.disposalMethod() == null || req.disposalMethod().isBlank()) { throw new ApiException(400, "处置方式(disposalMethod) 不能为空"); } w.setDisposalMethod(req.disposalMethod()); w.setDisposedDate(req.disposedDate() == null || req.disposedDate().isBlank() ? LocalDate.now().toString() : req.disposedDate()); w.setStatus("已处置"); return ApiResp.ok(wasteRepo.save(w)); } /** 对接环保台账:标记 syncEpb=true(模拟上报环保台账)。 */ @PostMapping("/wastes/{id}/sync-epb") @Transactional public ApiResp syncEpb(@PathVariable Long id) { LabWaste w = mustWaste(id); w.setSyncEpb(true); return ApiResp.ok(wasteRepo.save(w)); } // ==================== 安全巡检 ==================== @GetMapping("/safety-checks") public ApiResp> safetyChecks(@RequestParam(required = false) String status) { if (status != null && !status.isBlank()) { return ApiResp.ok(checkRepo.findByStatus(status)); } return ApiResp.ok(checkRepo.findAll()); } /** 标准检查项清单(前端构建 checklist 用)。 */ @GetMapping("/safety-check-items") public ApiResp> safetyCheckItems() { return ApiResp.ok(CHECK_ITEMS); } public record PlanRequest(String area, String planDate, String inspector) { } /** 生成巡检计划:按标准项化清单建一张「待巡检」单,预置全部检查项(待巡检)。 */ @PostMapping("/safety-checks/plan") @Transactional public ApiResp planCheck(@RequestBody PlanRequest req) { if (req.area() == null || req.area().isBlank()) { throw new ApiException(400, "实验室区域(area) 不能为空"); } StringBuilder items = new StringBuilder("["); for (int i = 0; i < CHECK_ITEMS.size(); i++) { if (i > 0) items.append(","); items.append("{\"item\":\"").append(CHECK_ITEMS.get(i)).append("\",\"ok\":null,\"note\":\"\"}"); } items.append("]"); LabSafetyCheck c = new LabSafetyCheck(); c.setCheckNo("SC-" + (checkRepo.count() + 1)); c.setArea(req.area()); c.setPlanDate(req.planDate() == null || req.planDate().isBlank() ? LocalDate.now().toString() : req.planDate()); c.setInspector(req.inspector()); c.setItemsJson(items.toString()); c.setAbnormalCount(0); c.setStatus("待巡检"); c.setCreatedAt(Instant.now()); return ApiResp.ok(checkRepo.save(c)); } public record SubmitCheckRequest(String inspector, String itemsJson, int abnormalCount, String remark) { } /** 提交巡检结果:写逐项结果与异常数,置「已完成」。 */ @PostMapping("/safety-checks/{id}/submit") @Transactional public ApiResp submitCheck(@PathVariable Long id, @RequestBody SubmitCheckRequest req) { LabSafetyCheck c = mustCheck(id); if ("已完成".equals(c.getStatus())) { throw new ApiException(409, "巡检已完成"); } if (req.inspector() != null && !req.inspector().isBlank()) c.setInspector(req.inspector()); if (req.itemsJson() != null && !req.itemsJson().isBlank()) c.setItemsJson(req.itemsJson()); c.setAbnormalCount(Math.max(0, req.abnormalCount())); c.setRemark(req.remark()); c.setCheckedDate(LocalDate.now().toString()); c.setStatus("已完成"); return ApiResp.ok(checkRepo.save(c)); } // ==================== 事件上报 ==================== @GetMapping("/incidents") public ApiResp> incidents(@RequestParam(required = false) String status) { if (status != null && !status.isBlank()) { return ApiResp.ok(incidentRepo.findByStatus(status)); } return ApiResp.ok(incidentRepo.findAll()); } public record IncidentRequest(String incidentType, String severity, String area, String occurredAt, String reporter, String description) { } /** 上报事件(落「上报」)。 */ @PostMapping("/incidents") @Transactional public ApiResp reportIncident(@RequestBody IncidentRequest req) { if (req.incidentType() == null || req.incidentType().isBlank()) { throw new ApiException(400, "事件类型(incidentType) 不能为空"); } LabIncident i = new LabIncident(); i.setIncidentNo("LI-" + (incidentRepo.count() + 1)); i.setIncidentType(req.incidentType()); i.setSeverity(req.severity() == null || req.severity().isBlank() ? "轻微" : req.severity()); i.setArea(req.area()); i.setOccurredAt(req.occurredAt()); i.setReporter(req.reporter()); i.setDescription(req.description()); i.setStatus("上报"); i.setCreatedAt(Instant.now()); return ApiResp.ok(incidentRepo.save(i)); } public record HandleIncidentRequest(String handler, String rectification) { } /** 处理:上报 → 处理中 → 已整改(写整改措施)。 */ @PostMapping("/incidents/{id}/handle") @Transactional public ApiResp handleIncident(@PathVariable Long id, @RequestBody HandleIncidentRequest req) { LabIncident i = mustIncident(id); if ("已关闭".equals(i.getStatus())) { throw new ApiException(409, "事件已关闭"); } if (req.handler() != null) i.setHandler(req.handler()); if (req.rectification() != null) i.setRectification(req.rectification()); i.setStatus(req.rectification() != null && !req.rectification().isBlank() ? "已整改" : "处理中"); return ApiResp.ok(incidentRepo.save(i)); } /** 关闭:已整改 → 已关闭。 */ @PostMapping("/incidents/{id}/close") @Transactional public ApiResp closeIncident(@PathVariable Long id) { LabIncident i = mustIncident(id); if (!"已整改".equals(i.getStatus())) { throw new ApiException(409, "仅已整改的事件可关闭,当前:" + i.getStatus()); } i.setStatus("已关闭"); i.setClosedDate(LocalDate.now().toString()); return ApiResp.ok(incidentRepo.save(i)); } // ---------- helpers ---------- private LabWaste mustWaste(Long id) { return wasteRepo.findById(id) .orElseThrow(() -> new NotFoundException("lab waste not found: " + id)); } private LabSafetyCheck mustCheck(Long id) { return checkRepo.findById(id) .orElseThrow(() -> new NotFoundException("safety check not found: " + id)); } private LabIncident mustIncident(Long id) { return incidentRepo.findById(id) .orElseThrow(() -> new NotFoundException("lab incident not found: " + id)); } }