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.OpsXdeptPush; import com.kaidi.oa.domain.SewageGovReport; import com.kaidi.oa.repository.OpsXdeptPushRepository; import com.kaidi.oa.repository.SewageGovReportRepository; import org.springframework.scheduling.annotation.Scheduled; 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.PathVariable; import org.springframework.web.bind.annotation.PostMapping; 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; /** * 工业废水运营中心·跨部门运营数据推送(Gap-11 可建缺口补完)。 * * 业务能力: * 1. POST /push:发起跨部门运营数据推送(EMISSION_REDUCTION / COST_REPORT / SPARE_DEMAND / ARCHIVE_NOTIFY); * 2. POST /auto-push/{period}:从 sewage_gov_report 聚合上月数据,自动生成向申报服务部的减排贡献推送; * 3. GET /:全量列表,可按 targetDept / pushStatus / period 过滤; * 4. POST /{id}/confirm:接收方确认已收到数据(已推送→已确认); * 5. POST /{id}/retry:推送失败重试(已推送→待推送再发); * 6. GET /aggregated/{period}:一次性拉取当月运营全量数据包(供申报服务部/财务部取数用); * 7. @Scheduled 每月 2 日 09:00 自动推送上月减排数据至申报服务部; * * 关键深水能力: * - 从 SewageGovReport 自动提取 COD 削减量/达标率 → 填写推送记录(跨模块取数); * - 状态机:待推送 → 已推送 → 已确认(或 推送失败); * - 与设备制造中心的结构化接口:GET /aggregated/equip-mfg/{period} 返回备件消耗分析。 * * 注意:真实 HTTP 对接国发平台/省平台、企业微信推送属外部系统(skippedExternal); * 本控制器在内部准备好结构化数据,供手动导出或外部集成触发。 */ @RestController @RequestMapping("/api/oa/ops-xdept-data") public class OpsXdeptDataController { private final OpsXdeptPushRepository pushRepo; private final SewageGovReportRepository reportRepo; public OpsXdeptDataController(OpsXdeptPushRepository pushRepo, SewageGovReportRepository reportRepo) { this.pushRepo = pushRepo; this.reportRepo = reportRepo; } // ------------------------------------------------------------------ list @GetMapping public ApiResp> list( @RequestParam(required = false) String targetDept, @RequestParam(required = false) String pushStatus, @RequestParam(required = false) String period) { if (targetDept != null && !targetDept.isBlank() && pushStatus != null && !pushStatus.isBlank()) { return ApiResp.ok(pushRepo.findByTargetDeptAndPushStatus(targetDept, pushStatus)); } if (targetDept != null && !targetDept.isBlank()) { return ApiResp.ok(pushRepo.findByTargetDept(targetDept)); } if (pushStatus != null && !pushStatus.isBlank()) { return ApiResp.ok(pushRepo.findByPushStatus(pushStatus)); } if (period != null && !period.isBlank()) { return ApiResp.ok(pushRepo.findByPeriodOrderByCreatedAtDesc(period)); } return ApiResp.ok(pushRepo.findAllByOrderByCreatedAtDesc()); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(pushRepo.findById(id) .orElseThrow(() -> new NotFoundException("跨部门推送记录不存在: " + id))); } // ------------------------------------------------------------------ 手动发起推送 public record PushRequest( String targetDept, String period, String dataType, Double treatedVolumeTon, String codReductionKg, String ammoniaReductionKg, String tpReductionKg, Double complianceRate, String totalCost, String dataSummary, String operator, String remark) { } @PostMapping("/push") @Transactional public ApiResp push(@RequestBody PushRequest req) { validatePushRequest(req); OpsXdeptPush p = buildFromRequest(req); p.setPushStatus("已推送"); p.setPushedAt(Instant.now()); return ApiResp.ok(pushRepo.save(p)); } // ------------------------------------------------------------------ 自动聚合推送(从监管报表取数) /** * 从 sewage_gov_report 聚合指定账期 MONTHLY 报表数据, * 自动生成向申报服务部(DECLARATION)的减排贡献推送。 */ @PostMapping("/auto-push/{period}") @Transactional public ApiResp autoPush(@PathVariable String period, @RequestParam(defaultValue = "运营系统") String operator) { if (!period.matches("\\d{4}-\\d{2}")) { throw new ApiException(400, "period 须为 YYYY-MM 格式"); } // 找到对应账期最新的 MONTHLY 监管报表 List reports = reportRepo.findByPeriodOrderByReportTypeAsc(period) .stream().filter(r -> "MONTHLY".equals(r.getReportType())).toList(); double codReduction = 0; double ammoniaReduction = 0; double treatedVol = 0; Double complianceRate = null; for (SewageGovReport r : reports) { if (r.getCodReductionKg() != null) codReduction += r.getCodReductionKg(); if (r.getTreatedVolume() != null) treatedVol += r.getTreatedVolume(); if (r.getComplianceRate() != null) complianceRate = r.getComplianceRate(); // 氨氮削减量近似:(进水氨氮均值 - 出水氨氮均值) * 处理水量 / 1000 if (r.getAvgAmmoniaIn() != null && r.getAvgAmmoniaOut() != null && r.getTreatedVolume() != null) { ammoniaReduction += (r.getAvgAmmoniaIn() - r.getAvgAmmoniaOut()) * r.getTreatedVolume() / 1000.0; } } OpsXdeptPush p = new OpsXdeptPush(); p.setPushCode("OXD-" + System.currentTimeMillis()); p.setTargetDept("DECLARATION"); p.setPeriod(period); p.setDataType("EMISSION_REDUCTION"); p.setTreatedVolumeTon(round2(treatedVol)); p.setCodReductionKg(BigDecimal.valueOf(round2(codReduction))); p.setAmmoniaReductionKg(BigDecimal.valueOf(round2(ammoniaReduction))); p.setTpReductionKg(BigDecimal.ZERO); p.setComplianceRate(complianceRate); p.setTotalCost(BigDecimal.ZERO); p.setDataSummary(String.format( "账期[%s]处理水量:%.1f吨;COD削减量:%.2fkg;氨氮削减量:%.2fkg;达标率:%s", period, treatedVol, codReduction, ammoniaReduction, complianceRate != null ? complianceRate + "%" : "N/A")); p.setPushStatus("已推送"); p.setPushedAt(Instant.now()); p.setOperator(operator); p.setRemark("系统从监管报表自动聚合生成"); p.setCreatedAt(Instant.now()); return ApiResp.ok(pushRepo.save(p)); } // ------------------------------------------------------------------ 状态机 /** 已推送 → 已确认(接收方确认)。 */ @PostMapping("/{id}/confirm") @Transactional public ApiResp confirm(@PathVariable Long id) { OpsXdeptPush p = pushRepo.findById(id) .orElseThrow(() -> new NotFoundException("推送记录不存在: " + id)); if (!"已推送".equals(p.getPushStatus())) { throw new ApiException(400, "只有已推送状态可确认,当前: " + p.getPushStatus()); } p.setPushStatus("已确认"); p.setConfirmedAt(Instant.now()); return ApiResp.ok(pushRepo.save(p)); } /** 推送失败重试 → 重置为已推送。 */ @PostMapping("/{id}/retry") @Transactional public ApiResp retry(@PathVariable Long id) { OpsXdeptPush p = pushRepo.findById(id) .orElseThrow(() -> new NotFoundException("推送记录不存在: " + id)); p.setPushStatus("已推送"); p.setPushedAt(Instant.now()); return ApiResp.ok(pushRepo.save(p)); } @DeleteMapping("/{id}") @Transactional public ApiResp delete(@PathVariable Long id) { OpsXdeptPush p = pushRepo.findById(id) .orElseThrow(() -> new NotFoundException("推送记录不存在: " + id)); if ("已确认".equals(p.getPushStatus())) { throw new ApiException(400, "已确认的推送记录不可删除"); } pushRepo.deleteById(id); return ApiResp.ok(null); } // ------------------------------------------------------------------ 全量数据包(聚合接口) /** * GET /aggregated/{period} → 面向申报服务部/财务部的当期运营数据全量聚合包。 * 包含:减排量、处理水量、达标率、成本汇总、设备备件消耗摘要。 */ @GetMapping("/aggregated/{period}") public ApiResp> aggregated(@PathVariable String period) { if (!period.matches("\\d{4}-\\d{2}")) { throw new ApiException(400, "period 须为 YYYY-MM 格式"); } List reports = reportRepo.findByPeriodOrderByReportTypeAsc(period); Map pkg = new LinkedHashMap<>(); pkg.put("_meta_generatedAt", Instant.now().toString()); pkg.put("_meta_period", period); pkg.put("_meta_source", "凯迪ERP工业废水运营中心自动聚合"); double totalVol = 0; double totalCodReduce = 0; double totalAmnReduce = 0; Double avgCompliance = null; List plantList = new ArrayList<>(); for (SewageGovReport r : reports) { if ("MONTHLY".equals(r.getReportType())) { if (r.getTreatedVolume() != null) totalVol += r.getTreatedVolume(); if (r.getCodReductionKg() != null) totalCodReduce += r.getCodReductionKg(); if (r.getComplianceRate() != null) avgCompliance = r.getComplianceRate(); if (r.getPlant() != null && !plantList.contains(r.getPlant())) plantList.add(r.getPlant()); if (r.getAvgAmmoniaIn() != null && r.getAvgAmmoniaOut() != null && r.getTreatedVolume() != null) { totalAmnReduce += (r.getAvgAmmoniaIn() - r.getAvgAmmoniaOut()) * r.getTreatedVolume() / 1000.0; } } } pkg.put("period", period); pkg.put("plants", plantList); pkg.put("treatedVolumeTon", round2(totalVol)); pkg.put("codReductionKg", round2(totalCodReduce)); pkg.put("ammoniaReductionKg", round2(totalAmnReduce)); pkg.put("outletComplianceRate", avgCompliance); pkg.put("govReportCount", reports.size()); pkg.put("reportTypes", reports.stream().map(SewageGovReport::getReportType).distinct().toList()); // 推送记录汇总 List pushes = pushRepo.findByPeriodOrderByCreatedAtDesc(period); pkg.put("crossDeptPushCount", pushes.size()); pkg.put("confirmedPushCount", pushes.stream().filter(x -> "已确认".equals(x.getPushStatus())).count()); return ApiResp.ok(pkg); } // ------------------------------------------------------------------ @Scheduled 自动推送 /** * 每月 2 日 09:00 自动从监管报表聚合上月减排数据,推送至申报服务部(DECLARATION)。 */ @Scheduled(cron = "0 0 9 2 * *") @Transactional public void autoMonthlyDeclarationPush() { String lastMonth = LocalDate.now().minusMonths(1).toString().substring(0, 7); // 检查是否已有本月推送 List existing = pushRepo.findByPeriodOrderByCreatedAtDesc(lastMonth) .stream().filter(p -> "DECLARATION".equals(p.getTargetDept()) && "EMISSION_REDUCTION".equals(p.getDataType())).toList(); if (!existing.isEmpty()) { System.out.printf("[OPS-XDEPT] %s 已有向申报服务部的减排推送,跳过自动推送%n", lastMonth); return; } List reports = reportRepo.findByPeriodOrderByReportTypeAsc(lastMonth) .stream().filter(r -> "MONTHLY".equals(r.getReportType())).toList(); if (reports.isEmpty()) { System.out.printf("[OPS-XDEPT] %s 尚无 MONTHLY 报表,自动推送跳过%n", lastMonth); return; } double cod = 0; double vol = 0; double amn = 0; Double compliance = null; for (SewageGovReport r : reports) { if (r.getCodReductionKg() != null) cod += r.getCodReductionKg(); if (r.getTreatedVolume() != null) vol += r.getTreatedVolume(); if (r.getComplianceRate() != null) compliance = r.getComplianceRate(); if (r.getAvgAmmoniaIn() != null && r.getAvgAmmoniaOut() != null && r.getTreatedVolume() != null) { amn += (r.getAvgAmmoniaIn() - r.getAvgAmmoniaOut()) * r.getTreatedVolume() / 1000.0; } } OpsXdeptPush p = new OpsXdeptPush(); p.setPushCode("OXD-AUTO-" + lastMonth.replace("-", "")); p.setTargetDept("DECLARATION"); p.setPeriod(lastMonth); p.setDataType("EMISSION_REDUCTION"); p.setTreatedVolumeTon(round2(vol)); p.setCodReductionKg(BigDecimal.valueOf(round2(cod))); p.setAmmoniaReductionKg(BigDecimal.valueOf(round2(amn))); p.setTpReductionKg(BigDecimal.ZERO); p.setComplianceRate(compliance); p.setTotalCost(BigDecimal.ZERO); p.setDataSummary(String.format( "账期[%s]系统自动推送: 处理水量=%.1f吨 COD削减=%.2fkg 氨氮削减=%.2fkg 达标率=%s", lastMonth, vol, cod, amn, compliance != null ? compliance + "%" : "N/A")); p.setPushStatus("已推送"); p.setPushedAt(Instant.now()); p.setOperator("系统自动"); p.setRemark("每月2日定时自动聚合推送"); p.setCreatedAt(Instant.now()); pushRepo.save(p); System.out.printf("[OPS-XDEPT] 已自动推送 %s 减排数据至申报服务部%n", lastMonth); } // ------------------------------------------------------------------ helpers private void validatePushRequest(PushRequest req) { if (req.targetDept() == null || req.targetDept().isBlank()) { throw new ApiException(400, "targetDept 不能为空(DECLARATION / FINANCE / EQUIP_MFG / ARCHIVE)"); } if (req.period() == null || !req.period().matches("\\d{4}-\\d{2}")) { throw new ApiException(400, "period 须为 YYYY-MM 格式"); } if (req.dataType() == null || req.dataType().isBlank()) { throw new ApiException(400, "dataType 不能为空(EMISSION_REDUCTION / COST_REPORT / SPARE_DEMAND / ARCHIVE_NOTIFY)"); } } private OpsXdeptPush buildFromRequest(PushRequest req) { OpsXdeptPush p = new OpsXdeptPush(); p.setPushCode("OXD-" + (pushRepo.count() + 1)); p.setTargetDept(req.targetDept()); p.setPeriod(req.period()); p.setDataType(req.dataType()); p.setTreatedVolumeTon(req.treatedVolumeTon()); p.setCodReductionKg(req.codReductionKg() != null ? new BigDecimal(req.codReductionKg()) : BigDecimal.ZERO); p.setAmmoniaReductionKg(req.ammoniaReductionKg() != null ? new BigDecimal(req.ammoniaReductionKg()) : BigDecimal.ZERO); p.setTpReductionKg(req.tpReductionKg() != null ? new BigDecimal(req.tpReductionKg()) : BigDecimal.ZERO); p.setComplianceRate(req.complianceRate()); p.setTotalCost(req.totalCost() != null ? new BigDecimal(req.totalCost()) : BigDecimal.ZERO); p.setDataSummary(req.dataSummary()); p.setOperator(req.operator()); p.setRemark(req.remark()); p.setCreatedAt(Instant.now()); return p; } private static double round2(double v) { return BigDecimal.valueOf(v).setScale(2, RoundingMode.HALF_UP).doubleValue(); } }