Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/AlertController.java
T
QiufengandClaude Opus 4.8 5e51dc3f56 SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:15 +08:00

221 lines
9.7 KiB
Java

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<Alert>> list(@RequestParam(required = false) String level) {
List<Alert> all = aggregate();
if (level == null || level.isBlank()) {
return ApiResp.ok(all);
}
List<Alert> 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<Alert> aggregate() {
List<Alert> 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;
}
}
}