恢复点(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>
222 lines
9.8 KiB
Java
222 lines
9.8 KiB
Java
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);
|
|
}
|
|
}
|