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:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
@@ -0,0 +1,221 @@
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.FertGasEmission;
import com.kaidi.oa.repository.FertGasEmissionRepository;
import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
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.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 生物质肥料制造中心·EHS 废气排放监测(需求 §8,补深审计 PARTIAL[med])。
*
* <ul>
* <li>按排放口录入监测数据(NH₃/H₂S/臭气指数/粉尘浓度),与限值对标后自动判定超标并拼装超标详情;</li>
* <li>GET /alerts → 超标未整改记录列表(环保预警核心);</li>
* <li>POST /{id}/rectify → 填整改措施,标记已整改;</li>
* <li>GET /trend?outlet=&from=&to= → 指定排放口按日期区间折线趋势(NH₃/H₂S/臭气历史数据);</li>
* <li>GET /summary → 各排放口超标次数汇总(EHS 看板)。</li>
* </ul>
*
* 写口受 AuthInterceptor default-deny(ADMIN/APPROVER) 保护,前缀已登记入 FINANCE/SENSITIVE_READ。
*/
@RestController
@RequestMapping("/api/oa/fert-gas-emissions")
public class FertGasEmissionController {
private final FertGasEmissionRepository repo;
public FertGasEmissionController(FertGasEmissionRepository repo) {
this.repo = repo;
}
// ---------- 列表 ----------
@GetMapping
public ApiResp<List<FertGasEmission>> list(@RequestParam(required = false) String outlet,
@RequestParam(required = false) Boolean overOnly) {
if (Boolean.TRUE.equals(overOnly)) return ApiResp.ok(repo.findByOverLimitTrue());
if (outlet != null && !outlet.isBlank()) return ApiResp.ok(repo.findByOutletCode(outlet.trim()));
return ApiResp.ok(repo.findAll());
}
/** 超标未整改列表(环保预警)。 */
@GetMapping("/alerts")
public ApiResp<List<FertGasEmission>> alerts() {
return ApiResp.ok(repo.findByRectifiedFalseAndOverLimitTrue());
}
@GetMapping("/{id}")
public ApiResp<FertGasEmission> get(@PathVariable Long id) {
return ApiResp.ok(find(id));
}
// ---------- 录入 / 更新 ----------
public record EmissionRequest(String monitorDate, String timePeriod, String outletCode, String outletDesc,
Double nh3Concentration, Double nh3Limit,
Double h2sConcentration, Double h2sLimit,
Double odorIndex, Double odorLimit,
Double dustConcentration, Double dustLimit,
String dataSource, Boolean deodorizerNormal,
String recorder, String remark) {
}
@PostMapping
public ApiResp<FertGasEmission> create(@RequestBody EmissionRequest req) {
if (req.outletCode() == null || req.outletCode().isBlank()) {
throw new ApiException(400, "排放口编号(outletCode) 不能为空");
}
FertGasEmission e = new FertGasEmission();
e.setCreatedAt(Instant.now());
apply(e, req);
return ApiResp.ok(repo.save(e));
}
@PutMapping("/{id}")
public ApiResp<FertGasEmission> update(@PathVariable Long id, @RequestBody EmissionRequest req) {
FertGasEmission e = find(id);
apply(e, req);
return ApiResp.ok(repo.save(e));
}
@DeleteMapping("/{id}")
public ApiResp<Void> delete(@PathVariable Long id) {
repo.delete(find(id));
return ApiResp.ok();
}
private void apply(FertGasEmission e, EmissionRequest req) {
e.setMonitorDate(req.monitorDate() == null || req.monitorDate().isBlank()
? LocalDate.now().toString() : req.monitorDate());
e.setTimePeriod(req.timePeriod());
if (req.outletCode() != null && !req.outletCode().isBlank()) e.setOutletCode(req.outletCode().trim());
e.setOutletDesc(req.outletDesc());
e.setNh3Concentration(bd(req.nh3Concentration()));
e.setNh3Limit(bd(req.nh3Limit()));
e.setH2sConcentration(bd(req.h2sConcentration()));
e.setH2sLimit(bd(req.h2sLimit()));
e.setOdorIndex(bd(req.odorIndex()));
e.setOdorLimit(bd(req.odorLimit()));
e.setDustConcentration(bd(req.dustConcentration()));
e.setDustLimit(bd(req.dustLimit()));
e.setDataSource(req.dataSource() == null || req.dataSource().isBlank() ? "手工录入" : req.dataSource());
e.setDeodorizerNormal(req.deodorizerNormal() == null ? true : req.deodorizerNormal());
e.setRecorder(req.recorder());
e.setRemark(req.remark());
// 自动判定超标。
judgeOverLimit(e);
}
/** 逐项比对限值,自动设 overLimit + overDetail。 */
private static void judgeOverLimit(FertGasEmission e) {
List<String> items = new ArrayList<>();
if (e.getNh3Limit().signum() > 0 && e.getNh3Concentration().compareTo(e.getNh3Limit()) > 0) {
items.add("NH3超标:" + e.getNh3Concentration() + ">" + e.getNh3Limit() + "mg/m³");
}
if (e.getH2sLimit().signum() > 0 && e.getH2sConcentration().compareTo(e.getH2sLimit()) > 0) {
items.add("H2S超标:" + e.getH2sConcentration() + ">" + e.getH2sLimit() + "mg/m³");
}
if (e.getOdorLimit().signum() > 0 && e.getOdorIndex().compareTo(e.getOdorLimit()) > 0) {
items.add("臭气超标:" + e.getOdorIndex() + ">" + e.getOdorLimit());
}
if (e.getDustLimit().signum() > 0 && e.getDustConcentration().compareTo(e.getDustLimit()) > 0) {
items.add("粉尘超标:" + e.getDustConcentration() + ">" + e.getDustLimit() + "mg/m³");
}
e.setOverLimit(!items.isEmpty());
e.setOverDetail(items.isEmpty() ? null : String.join("; ", items));
if (!items.isEmpty()) e.setRectified(false);
}
// ---------- 整改 ----------
public record RectifyRequest(String rectifyNote) {
}
@PostMapping("/{id}/rectify")
public ApiResp<FertGasEmission> rectify(@PathVariable Long id, @RequestBody RectifyRequest req) {
FertGasEmission e = find(id);
if (!Boolean.TRUE.equals(e.getOverLimit())) {
throw new ApiException(409, "该记录未超标,无需整改");
}
if (req.rectifyNote() == null || req.rectifyNote().isBlank()) {
throw new ApiException(400, "整改措施(rectifyNote) 不能为空");
}
e.setRectifyNote(req.rectifyNote().trim());
e.setRectified(true);
return ApiResp.ok(repo.save(e));
}
// ---------- 趋势(按排放口 + 日期区间) ----------
public record TrendPoint(String monitorDate, BigDecimal nh3, BigDecimal h2s, BigDecimal odorIndex,
BigDecimal dust, Boolean overLimit) {
}
@GetMapping("/trend")
public ApiResp<List<TrendPoint>> trend(@RequestParam(required = false) String outlet,
@RequestParam(required = false) String from,
@RequestParam(required = false) String to) {
String f = from == null || from.isBlank() ? LocalDate.now().minusMonths(1).toString() : from;
String t = to == null || to.isBlank() ? LocalDate.now().toString() : to;
List<FertGasEmission> data = repo.findByMonitorDateBetweenOrderByMonitorDateDesc(f, t);
return ApiResp.ok(data.stream()
.filter(e -> outlet == null || outlet.isBlank() || outlet.trim().equals(e.getOutletCode()))
.map(e -> new TrendPoint(e.getMonitorDate(), e.getNh3Concentration(),
e.getH2sConcentration(), e.getOdorIndex(), e.getDustConcentration(), e.getOverLimit()))
.toList());
}
// ---------- 各排放口超标汇总(EHS 看板) ----------
public record OutletSummary(String outletCode, String outletDesc, long totalRecords,
long overCount, long pendingRectify) {
}
@GetMapping("/summary")
public ApiResp<List<OutletSummary>> summary() {
Map<String, long[]> acc = new LinkedHashMap<>(); // [total, over, pendingRectify]
Map<String, String> descMap = new LinkedHashMap<>();
for (FertGasEmission e : repo.findAll()) {
String code = e.getOutletCode() == null ? "未知" : e.getOutletCode();
long[] v = acc.computeIfAbsent(code, k -> new long[]{0, 0, 0});
v[0]++;
if (Boolean.TRUE.equals(e.getOverLimit())) {
v[1]++;
if (!Boolean.TRUE.equals(e.getRectified())) v[2]++;
}
descMap.putIfAbsent(code, e.getOutletDesc());
}
return ApiResp.ok(acc.entrySet().stream()
.map(en -> new OutletSummary(en.getKey(), descMap.get(en.getKey()),
en.getValue()[0], en.getValue()[1], en.getValue()[2]))
.toList());
}
// ---------- helpers ----------
private FertGasEmission find(Long id) {
return repo.findById(id).orElseThrow(() -> new NotFoundException("废气排放记录不存在:" + id));
}
private static BigDecimal bd(Double v) {
return v == null ? BigDecimal.ZERO : BigDecimal.valueOf(v);
}
}