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.SewageCostSavingProject; import com.kaidi.oa.repository.SewageCostSavingProjectRepository; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; 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.util.List; import java.util.concurrent.atomic.AtomicLong; /** * 降本增效项目台账(城镇污水运营·绩效与成本分析)。 * * 补深审计 low 缺口:降本增效项目需独立台账,记录节能技改项目 * 立项+改造前基期+实际节约额计算;当前 KPI 聚合仅有文字建议,无项目级对账。 * * 关键计算逻辑(写入时在控制器自动执行,前端无需传计算字段): * savedKwhPerMonth = (baseUnitPower - actualUnitPower) × actualMonthlyWater * savedKwhTotal = savedKwhPerMonth × verifyMonths * savedReagentKgPerMonth = (baseUnitReagent - actualUnitReagent)/1000 × actualMonthlyWater * savedPowerCost = savedKwhTotal × powerPrice(BigDecimal) * savedReagentCost = savedReagentKgPerMonth × verifyMonths × reagentPrice * savedTotalCost = savedPowerCost + savedReagentCost * paybackMonths = investCost / (savedTotalCost / verifyMonths) * * 路径:/api/oa/sewage-cost-saving-projects */ @RestController @RequestMapping("/api/oa/sewage-cost-saving-projects") public class SewageCostSavingProjectController { private final SewageCostSavingProjectRepository repo; private static final AtomicLong seq = new AtomicLong(1); public SewageCostSavingProjectController(SewageCostSavingProjectRepository repo) { this.repo = repo; } // ---------- 请求体 ---------- record ProjectReq( String projectName, String projectType, String plant, String kickoffDate, String planFinishDate, String actualFinishDate, String investCost, Integer baseMonths, Double baseUnitPower, Double baseUnitReagent, Double baseMonthlyWater, Integer verifyMonths, Double actualUnitPower, Double actualUnitReagent, Double actualMonthlyWater, String powerPrice, String reagentPrice, String projStatus, String owner, String remark ) {} record StatusReq(String projStatus) {} // ---------- 汇总响应 ---------- public record ProjectSummary( long total, long active, long completed, BigDecimal totalInvestCost, BigDecimal totalSavedCost, BigDecimal totalSavedPowerCost, BigDecimal totalSavedReagentCost, double totalSavedKwh, String roiNote ) {} // ---------- CRUD ---------- @GetMapping public ApiResp> list( @RequestParam(required = false) String plant, @RequestParam(required = false) String projStatus, @RequestParam(required = false) String projectType) { List all; if (plant != null && !plant.isBlank()) { all = repo.findByPlant(plant); } else if (projStatus != null && !projStatus.isBlank()) { all = repo.findByProjStatus(projStatus); } else if (projectType != null && !projectType.isBlank()) { all = repo.findByProjectType(projectType); } else { all = repo.findAll(); } return ApiResp.ok(all); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(repo.findById(id).orElseThrow(() -> new NotFoundException("项目不存在"))); } @PostMapping @Transactional public ApiResp create(@RequestBody ProjectReq req) { if (req.projectName() == null || req.projectName().isBlank()) { throw new ApiException(400, "项目名称不能为空"); } if (req.kickoffDate() == null || req.kickoffDate().isBlank()) { throw new ApiException(400, "立项日期不能为空"); } SewageCostSavingProject p = applyReq(new SewageCostSavingProject(), req); long count = repo.count(); p.setCode("JNJ-" + String.format("%04d", count + seq.getAndIncrement())); p.setCreatedAt(Instant.now()); p.setUpdatedAt(Instant.now()); recalc(p); return ApiResp.ok(repo.save(p)); } @PutMapping("/{id}") @Transactional public ApiResp update(@PathVariable Long id, @RequestBody ProjectReq req) { SewageCostSavingProject p = repo.findById(id).orElseThrow(() -> new NotFoundException("项目不存在")); applyReq(p, req); p.setUpdatedAt(Instant.now()); recalc(p); return ApiResp.ok(repo.save(p)); } @PatchMapping("/{id}/status") @Transactional public ApiResp updateStatus(@PathVariable Long id, @RequestBody StatusReq req) { SewageCostSavingProject p = repo.findById(id).orElseThrow(() -> new NotFoundException("项目不存在")); List validStatuses = List.of("立项中", "改造中", "效果验证", "已完结", "暂停"); if (!validStatuses.contains(req.projStatus())) { throw new ApiException(400, "无效状态,允许值:" + String.join("/", validStatuses)); } p.setProjStatus(req.projStatus()); p.setUpdatedAt(Instant.now()); return ApiResp.ok(repo.save(p)); } @DeleteMapping("/{id}") @Transactional public ApiResp delete(@PathVariable Long id) { if (!repo.existsById(id)) throw new NotFoundException("项目不存在"); repo.deleteById(id); return ApiResp.ok("已删除"); } // ---------- 汇总统计 ---------- @GetMapping("/summary") public ApiResp summary(@RequestParam(required = false) String plant) { List all = plant != null && !plant.isBlank() ? repo.findByPlant(plant) : repo.findAll(); long total = all.size(); long active = all.stream().filter(p -> List.of("改造中", "效果验证", "立项中").contains(p.getProjStatus())).count(); long completed = all.stream().filter(p -> "已完结".equals(p.getProjStatus())).count(); BigDecimal totalInvest = all.stream() .map(p -> p.getInvestCost() != null ? p.getInvestCost() : BigDecimal.ZERO) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal totalSaved = all.stream() .map(p -> p.getSavedTotalCost() != null ? p.getSavedTotalCost() : BigDecimal.ZERO) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal savedPower = all.stream() .map(p -> p.getSavedPowerCost() != null ? p.getSavedPowerCost() : BigDecimal.ZERO) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal savedReagent = all.stream() .map(p -> p.getSavedReagentCost() != null ? p.getSavedReagentCost() : BigDecimal.ZERO) .reduce(BigDecimal.ZERO, BigDecimal::add); double totalKwh = all.stream() .mapToDouble(p -> p.getSavedKwhTotal() != null ? p.getSavedKwhTotal() : 0.0).sum(); String roiPct; if (totalInvest.compareTo(BigDecimal.ZERO) > 0) { roiPct = totalSaved.multiply(BigDecimal.valueOf(100)) .divide(totalInvest, 1, RoundingMode.HALF_UP) + "%"; } else { roiPct = "—"; } String roi = totalInvest.compareTo(BigDecimal.ZERO) == 0 ? "暂无投入数据" : "累计投入 " + formatMoney(totalInvest) + " 元,累计节约 " + formatMoney(totalSaved) + " 元,静态回报率 " + roiPct; return ApiResp.ok(new ProjectSummary(total, active, completed, totalInvest, totalSaved, savedPower, savedReagent, Math.round(totalKwh * 10.0) / 10.0, roi)); } // ---------- 节约核算:重算一条 ---------- @PostMapping("/{id}/recalc") @Transactional public ApiResp triggerRecalc(@PathVariable Long id) { SewageCostSavingProject p = repo.findById(id).orElseThrow(() -> new NotFoundException("项目不存在")); recalc(p); p.setUpdatedAt(Instant.now()); return ApiResp.ok(repo.save(p)); } // ---------- helpers ---------- private SewageCostSavingProject applyReq(SewageCostSavingProject p, ProjectReq req) { p.setProjectName(req.projectName()); p.setProjectType(req.projectType()); p.setPlant(req.plant()); p.setKickoffDate(req.kickoffDate()); p.setPlanFinishDate(req.planFinishDate()); p.setActualFinishDate(req.actualFinishDate()); p.setInvestCost(parseBD(req.investCost())); p.setBaseMonths(req.baseMonths() != null ? req.baseMonths() : 3); p.setBaseUnitPower(req.baseUnitPower() != null ? req.baseUnitPower() : 0.0); p.setBaseUnitReagent(req.baseUnitReagent() != null ? req.baseUnitReagent() : 0.0); p.setBaseMonthlyWater(req.baseMonthlyWater() != null ? req.baseMonthlyWater() : 0.0); p.setVerifyMonths(req.verifyMonths() != null ? req.verifyMonths() : 3); p.setActualUnitPower(req.actualUnitPower() != null ? req.actualUnitPower() : 0.0); p.setActualUnitReagent(req.actualUnitReagent() != null ? req.actualUnitReagent() : 0.0); p.setActualMonthlyWater(req.actualMonthlyWater() != null ? req.actualMonthlyWater() : 0.0); p.setPowerPrice(parseBD(req.powerPrice())); p.setReagentPrice(parseBD(req.reagentPrice())); if (req.projStatus() != null) p.setProjStatus(req.projStatus()); p.setOwner(req.owner()); p.setRemark(req.remark()); return p; } /** * 自动计算节约量(使用 BigDecimal 乘法,避免浮点误差影响金额)。 * savedKwhPerMonth = (baseUnitPower - actualUnitPower) × actualMonthlyWater */ private void recalc(SewageCostSavingProject p) { double basePow = nvlD(p.getBaseUnitPower()); double actPow = nvlD(p.getActualUnitPower()); double actWater = nvlD(p.getActualMonthlyWater()); int verifyM = p.getVerifyMonths() != null ? p.getVerifyMonths() : 3; double deltaUnitPower = basePow - actPow; double savedKwhPerM = deltaUnitPower * actWater; if (savedKwhPerM < 0) savedKwhPerM = 0; double savedKwhTotal = savedKwhPerM * verifyM; double baseRea = nvlD(p.getBaseUnitReagent()); double actRea = nvlD(p.getActualUnitReagent()); double deltaUnitReagent = baseRea - actRea; double savedReagentKgPerM = deltaUnitReagent / 1000.0 * actWater; if (savedReagentKgPerM < 0) savedReagentKgPerM = 0; BigDecimal pp = nvlBD(p.getPowerPrice()); BigDecimal rp = nvlBD(p.getReagentPrice()); BigDecimal savedPowerCost = BigDecimal.valueOf(savedKwhTotal).multiply(pp) .setScale(2, RoundingMode.HALF_UP); BigDecimal savedReagentCost = BigDecimal.valueOf(savedReagentKgPerM * verifyM).multiply(rp) .setScale(2, RoundingMode.HALF_UP); BigDecimal savedTotal = savedPowerCost.add(savedReagentCost); p.setSavedKwhPerMonth(round2(savedKwhPerM)); p.setSavedKwhTotal(round2(savedKwhTotal)); p.setSavedReagentKgPerMonth(round2(savedReagentKgPerM)); p.setSavedPowerCost(savedPowerCost); p.setSavedReagentCost(savedReagentCost); p.setSavedTotalCost(savedTotal); BigDecimal invest = nvlBD(p.getInvestCost()); if (savedTotal.compareTo(BigDecimal.ZERO) > 0 && verifyM > 0) { BigDecimal monthlyRate = savedTotal.divide(BigDecimal.valueOf(verifyM), 4, RoundingMode.HALF_UP); if (monthlyRate.compareTo(BigDecimal.ZERO) > 0) { double pb = invest.divide(monthlyRate, 2, RoundingMode.HALF_UP).doubleValue(); p.setPaybackMonths(pb); } } } private static double nvlD(Double v) { return v != null ? v : 0.0; } private static BigDecimal nvlBD(BigDecimal v) { return v != null ? v : BigDecimal.ZERO; } private static double round2(double v) { return Math.round(v * 100.0) / 100.0; } private static String formatMoney(BigDecimal v) { return v != null ? v.setScale(0, RoundingMode.HALF_UP).toPlainString() : "0"; } private static BigDecimal parseBD(String s) { if (s == null || s.isBlank()) return BigDecimal.ZERO; try { return new BigDecimal(s.trim()).setScale(2, RoundingMode.HALF_UP); } catch (NumberFormatException e) { return BigDecimal.ZERO; } } }