Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/FertEnergyLogController.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

198 lines
8.7 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.FertEnergyLog;
import com.kaidi.oa.repository.FertEnergyLogRepository;
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.math.RoundingMode;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 生物质肥料制造中心·能源消耗逐班记录 + 分摊批次 + 单位能耗对标(需求 §7,补深审计 PARTIAL[med])。
*
* <ul>
* <li>逐班录入电/燃料/水耗,系统自动算综合能耗(kgce)与单位产品能耗(kgce/吨),并与行业基准对标超标预警;</li>
* <li>GET /by-batch?batchRef= 把同一批次/工单各班能耗合计,得到该批次单位能耗(分摊到生产批次);</li>
* <li>GET /benchmark 按工序汇总单耗与超标条数,干燥工序重点监控。</li>
* </ul>
* 综合能耗折标系数:电 1kWh≈0.1229kgce;燃料按填写的折标系数 fuelCoalFactor × fuelQty × 1000(吨标煤→kgce)。
* 写口受 AuthInterceptor default-deny(ADMIN/APPROVER) 保护,前缀已登记入 FINANCE/SENSITIVE_READ。
*/
@RestController
@RequestMapping("/api/oa/fert-energy-logs")
public class FertEnergyLogController {
/** 电力折标煤系数 kgce/kWh(当量值 0.1229)。 */
private static final BigDecimal ELEC_FACTOR = new BigDecimal("0.1229");
private final FertEnergyLogRepository repo;
public FertEnergyLogController(FertEnergyLogRepository repo) {
this.repo = repo;
}
@GetMapping
public ApiResp<List<FertEnergyLog>> list(@RequestParam(required = false) String process,
@RequestParam(required = false) Boolean overOnly) {
if (Boolean.TRUE.equals(overOnly)) return ApiResp.ok(repo.findByOverBenchmarkTrue());
if (process != null && !process.isBlank()) return ApiResp.ok(repo.findByProcess(process));
return ApiResp.ok(repo.findAll());
}
@GetMapping("/{id}")
public ApiResp<FertEnergyLog> get(@PathVariable Long id) {
return ApiResp.ok(find(id));
}
public record EnergyRequest(String logDate, String shift, String process, String batchRef,
Double outputQty, Double electricityKwh, String fuelType, Double fuelQty,
Double fuelCoalFactor, Double waterTon, Double benchmarkKgcePerTon,
String recorder, String remark) {
}
@PostMapping
public ApiResp<FertEnergyLog> create(@RequestBody EnergyRequest req) {
FertEnergyLog l = new FertEnergyLog();
l.setCreatedAt(Instant.now());
apply(l, req);
return ApiResp.ok(repo.save(l));
}
@PutMapping("/{id}")
public ApiResp<FertEnergyLog> update(@PathVariable Long id, @RequestBody EnergyRequest req) {
FertEnergyLog l = find(id);
apply(l, req);
return ApiResp.ok(repo.save(l));
}
@DeleteMapping("/{id}")
public ApiResp<Void> delete(@PathVariable Long id) {
repo.delete(find(id));
return ApiResp.ok();
}
private void apply(FertEnergyLog l, EnergyRequest req) {
l.setLogDate(req.logDate() == null || req.logDate().isBlank() ? LocalDate.now().toString() : req.logDate());
l.setShift(req.shift() == null || req.shift().isBlank() ? "全天" : req.shift());
l.setProcess(req.process());
l.setBatchRef(req.batchRef());
l.setOutputQty(bd(req.outputQty()));
l.setElectricityKwh(bd(req.electricityKwh()));
l.setFuelType(req.fuelType());
l.setFuelQty(bd(req.fuelQty()));
l.setFuelCoalFactor(bd(req.fuelCoalFactor()));
l.setWaterTon(bd(req.waterTon()));
l.setBenchmarkKgcePerTon(bd(req.benchmarkKgcePerTon()));
l.setRecorder(req.recorder());
l.setRemark(req.remark());
recompute(l);
}
/** 自动计算综合能耗、单位能耗、超标判定。 */
private void recompute(FertEnergyLog l) {
// 电力折标:kWh × 0.1229 = kgce。
BigDecimal elecKgce = l.getElectricityKwh().multiply(ELEC_FACTOR);
// 燃料折标:fuelQty(吨等) × fuelCoalFactor(吨标煤/单位) × 1000(吨→千克标煤)。
BigDecimal fuelKgce = l.getFuelQty().multiply(l.getFuelCoalFactor()).multiply(new BigDecimal("1000"));
BigDecimal total = elecKgce.add(fuelKgce).setScale(2, RoundingMode.HALF_UP);
l.setTotalKgce(total);
if (l.getOutputQty().signum() > 0) {
BigDecimal unit = total.divide(l.getOutputQty(), 2, RoundingMode.HALF_UP);
l.setUnitKgcePerTon(unit);
l.setOverBenchmark(l.getBenchmarkKgcePerTon().signum() > 0
&& unit.compareTo(l.getBenchmarkKgcePerTon()) > 0);
} else {
l.setUnitKgcePerTon(BigDecimal.ZERO);
l.setOverBenchmark(false);
}
}
// ---------- 批次能耗分摊 ----------
public record BatchEnergy(String batchRef, BigDecimal totalKgce, BigDecimal totalOutput,
BigDecimal unitKgcePerTon, BigDecimal electricityKwh, BigDecimal waterTon,
long logCount) {
}
@GetMapping("/by-batch")
public ApiResp<BatchEnergy> byBatch(@RequestParam String batchRef) {
if (batchRef == null || batchRef.isBlank()) {
throw new ApiException(400, "批次/工单号(batchRef) 不能为空");
}
List<FertEnergyLog> logs = repo.findByBatchRef(batchRef.trim());
BigDecimal kgce = BigDecimal.ZERO, out = BigDecimal.ZERO, elec = BigDecimal.ZERO, water = BigDecimal.ZERO;
for (FertEnergyLog l : logs) {
kgce = kgce.add(nz(l.getTotalKgce()));
out = out.add(nz(l.getOutputQty()));
elec = elec.add(nz(l.getElectricityKwh()));
water = water.add(nz(l.getWaterTon()));
}
BigDecimal unit = out.signum() > 0 ? kgce.divide(out, 2, RoundingMode.HALF_UP) : BigDecimal.ZERO;
return ApiResp.ok(new BatchEnergy(batchRef.trim(), scale(kgce), scale(out), unit, scale(elec),
scale(water), logs.size()));
}
// ---------- 工序单耗对标看板 ----------
public record ProcessBench(String process, BigDecimal totalKgce, BigDecimal totalOutput,
BigDecimal unitKgcePerTon, long overCount, long logCount) {
}
@GetMapping("/benchmark")
public ApiResp<List<ProcessBench>> benchmark() {
Map<String, BigDecimal[]> acc = new LinkedHashMap<>(); // [kgce, output, over, count]
for (FertEnergyLog l : repo.findAll()) {
String p = l.getProcess() == null || l.getProcess().isBlank() ? "未分类" : l.getProcess();
BigDecimal[] v = acc.computeIfAbsent(p, k -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO});
v[0] = v[0].add(nz(l.getTotalKgce()));
v[1] = v[1].add(nz(l.getOutputQty()));
if (Boolean.TRUE.equals(l.getOverBenchmark())) v[2] = v[2].add(BigDecimal.ONE);
v[3] = v[3].add(BigDecimal.ONE);
}
List<ProcessBench> rows = new ArrayList<>();
for (Map.Entry<String, BigDecimal[]> e : acc.entrySet()) {
BigDecimal[] v = e.getValue();
BigDecimal unit = v[1].signum() > 0 ? v[0].divide(v[1], 2, RoundingMode.HALF_UP) : BigDecimal.ZERO;
rows.add(new ProcessBench(e.getKey(), scale(v[0]), scale(v[1]), unit,
v[2].longValue(), v[3].longValue()));
}
rows.sort((a, b) -> b.unitKgcePerTon().compareTo(a.unitKgcePerTon()));
return ApiResp.ok(rows);
}
// ---------- helpers ----------
private FertEnergyLog 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).setScale(2, RoundingMode.HALF_UP);
}
private static BigDecimal nz(BigDecimal v) {
return v == null ? BigDecimal.ZERO : v;
}
private static BigDecimal scale(BigDecimal v) {
return nz(v).setScale(2, RoundingMode.HALF_UP);
}
}