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>
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user