package com.kaidi.oa.web; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.domain.Budget; import com.kaidi.oa.domain.ContractMilestone; import com.kaidi.oa.domain.FormInstance; import com.kaidi.oa.domain.InventoryItem; import com.kaidi.oa.domain.LabInstrument; import com.kaidi.oa.domain.Patent; import com.kaidi.oa.domain.PersonnelCert; import com.kaidi.oa.domain.SafetyCheck; import com.kaidi.oa.repository.BudgetRepository; import com.kaidi.oa.repository.ContractMilestoneRepository; import com.kaidi.oa.repository.FormInstanceRepository; import com.kaidi.oa.repository.InventoryItemRepository; import com.kaidi.oa.repository.LabInstrumentRepository; import com.kaidi.oa.repository.PatentRepository; import com.kaidi.oa.repository.PersonnelCertRepository; import com.kaidi.oa.repository.SafetyCheckRepository; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; /** * Platform-wide alert aggregation (全平台期限/异常预警聚合). Scans the deadline * and exception state of multiple business centers and folds them into a single * flat alert feed. level is one of warning / danger. The aggregation is also * reused by AlertScheduler for the hourly push demonstration. */ @RestController @RequestMapping("/api/oa/alerts") public class AlertController { /** Maximum number of alerts returned in a single feed. */ private static final int MAX_ALERTS = 100; private final PersonnelCertRepository personnelCertRepo; private final PatentRepository patentRepo; private final ContractMilestoneRepository contractMilestoneRepo; private final InventoryItemRepository inventoryItemRepo; private final LabInstrumentRepository labInstrumentRepo; private final SafetyCheckRepository safetyCheckRepo; private final BudgetRepository budgetRepo; private final FormInstanceRepository instanceRepo; public AlertController(PersonnelCertRepository personnelCertRepo, PatentRepository patentRepo, ContractMilestoneRepository contractMilestoneRepo, InventoryItemRepository inventoryItemRepo, LabInstrumentRepository labInstrumentRepo, SafetyCheckRepository safetyCheckRepo, BudgetRepository budgetRepo, FormInstanceRepository instanceRepo) { this.personnelCertRepo = personnelCertRepo; this.patentRepo = patentRepo; this.contractMilestoneRepo = contractMilestoneRepo; this.inventoryItemRepo = inventoryItemRepo; this.labInstrumentRepo = labInstrumentRepo; this.safetyCheckRepo = safetyCheckRepo; this.budgetRepo = budgetRepo; this.instanceRepo = instanceRepo; } /** * A single platform alert. type is the alert category (证件预警 / 专利年费 / * 履约预警 / 库存预警 / 仪器校准 / 安全隐患 / 成本超支); level is warning or * danger; source is the originating entity name. */ public record Alert(String type, String level, String title, String desc, String dueDate, String source) { } @GetMapping public ApiResp> list(@RequestParam(required = false) String level) { List all = aggregate(); if (level == null || level.isBlank()) { return ApiResp.ok(all); } List filtered = new ArrayList<>(); for (Alert a : all) { if (level.equals(a.level())) { filtered.add(a); } } return ApiResp.ok(filtered); } /** * Scans every alert-source repository and folds the deadline / exception * state into a single flat list, capped at {@link #MAX_ALERTS} entries. * Public so AlertScheduler can reuse it for the hourly push demonstration. */ public List aggregate() { List out = new ArrayList<>(); LocalDate today = LocalDate.now(); LocalDate inOneYear = today.plusYears(1); // PersonnelCert - certificate validity (证件预警). for (PersonnelCert c : personnelCertRepo.findAll()) { String st = c.getStatus(); if ("即将到期".equals(st) || "已过期".equals(st)) { String level = "已过期".equals(st) ? "danger" : "warning"; out.add(new Alert("证件预警", level, c.getPersonName() + " " + c.getCertType() + " " + st, "所属部门 " + c.getDept() + ", 证件状态 " + st, c.getExpireDate(), "PersonnelCert")); } } // Patent - annual fee due within one year (专利年费). for (Patent p : patentRepo.findAll()) { String fee = p.getFeeDueDate(); if (fee != null && !fee.isBlank()) { LocalDate due = parseDate(fee); if (due != null && !due.isAfter(inOneYear)) { out.add(new Alert("专利年费", "warning", p.getName() + " 年费临近", "专利 " + p.getPatentNo() + " 年费应缴日 " + fee, fee, "Patent")); } } } // ContractMilestone - performance milestone overdue (履约预警). for (ContractMilestone m : contractMilestoneRepo.findAll()) { if ("逾期".equals(m.getStatus())) { out.add(new Alert("履约预警", "danger", "履约节点逾期: " + m.getName(), "合同 " + m.getContractId() + " 节点 " + m.getName() + " 已逾期", m.getDueDate(), "ContractMilestone")); } } // InventoryItem - stock below safety line (库存预警). for (InventoryItem it : inventoryItemRepo.findAll()) { if ("低于安全线".equals(it.getStatus())) { out.add(new Alert("库存预警", "warning", "库存低于安全线: " + it.getMaterialName(), "当前 " + it.getQuantity() + it.getUnit() + ", 安全库存 " + it.getSafetyStock() + it.getUnit(), "", "InventoryItem")); } } // LabInstrument - calibration due (仪器校准). Only instruments whose next // calibration falls inside the due window (overdue, or within 30 days) are // flagged, mirroring the other deadline sources. 待校准 status is always due. // 停用 / 维修 instruments are excluded — they are not in service. LocalDate calibrationWindow = today.plusDays(30); for (LabInstrument ins : labInstrumentRepo.findAll()) { String st = ins.getStatus(); if ("停用".equals(st) || "维修".equals(st)) { continue; } boolean pending = "待校准".equals(st); String next = ins.getNextCalibration(); LocalDate nextDue = parseDate(next); boolean dueSoon = nextDue != null && !nextDue.isAfter(calibrationWindow); if (pending || dueSoon) { String level = (nextDue != null && nextDue.isBefore(today)) || pending ? "danger" : "warning"; out.add(new Alert("仪器校准", level, "仪器待校准: " + ins.getName(), "资产编号 " + ins.getAssetNo() + ", 下次校准 " + (next == null ? "" : next), next == null ? "" : next, "LabInstrument")); } } // SafetyCheck - hidden danger not closed (安全隐患). for (SafetyCheck s : safetyCheckRepo.findAll()) { if (!"已闭环".equals(s.getRectifyStatus())) { String level = "重大".equals(s.getLevel()) ? "danger" : "warning"; out.add(new Alert("安全隐患", level, "隐患待整改: " + s.getItem(), "地点 " + s.getLocation() + ", 整改状态 " + s.getRectifyStatus(), s.getDueDate(), "SafetyCheck")); } } // FormInstance - approval timed out on its current node (审批超时), flagged by AlertScheduler. for (FormInstance inst : instanceRepo.findAll()) { if (Boolean.TRUE.equals(inst.getOvertime())) { out.add(new Alert("审批超时", "warning", "审批滞留: " + inst.getTitle(), "事项在节点「" + inst.getCurrentNode() + "」滞留超时,发起人 " + inst.getOriginUser(), "", "FormInstance")); } } // Budget - cost overrun (成本超支). for (Budget b : budgetRepo.findAll()) { if ("超支".equals(b.getStatus())) { out.add(new Alert("成本超支", "danger", "预算超支: " + b.getName(), "预算 " + b.getBudgetAmount() + ", 实际 " + b.getActualAmount(), "", "Budget")); } } if (out.size() > MAX_ALERTS) { return new ArrayList<>(out.subList(0, MAX_ALERTS)); } return out; } /** Parses a yyyy-MM-dd date, returning null on any non-parsable value. */ private LocalDate parseDate(String s) { try { return LocalDate.parse(s); } catch (RuntimeException ex) { return null; } } }