package com.kaidi.oa.web; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.common.Money; import com.kaidi.oa.common.NotFoundException; import com.kaidi.oa.domain.AdminProcurementRequest; import com.kaidi.oa.domain.ArApItem; import com.kaidi.oa.domain.Archive; import com.kaidi.oa.domain.LabSample; import com.kaidi.oa.domain.WwSamplingTask; import com.kaidi.oa.repository.AdminProcurementRequestRepository; import com.kaidi.oa.repository.ArApItemRepository; import com.kaidi.oa.repository.ArchiveRepository; import com.kaidi.oa.repository.LabSampleRepository; import com.kaidi.oa.repository.WwSamplingTaskRepository; import org.springframework.transaction.annotation.Transactional; 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.RestController; import java.math.BigDecimal; import java.time.Instant; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; /** * 工业废水运营中心·跨部门真实联动桥接控制器(Gap-11 补深)。 * * 将现有"联动记录标记"升级为真实跨模块写入: * * 1. POST /procurement-link/{wwSamplingTaskId} * WwSamplingTask 超标事件 → 真实写入 AdminProcurementRequest(药剂采购申请单)。 * 当检测结果超标时,系统自动向采购部生成结构化采购申请(中和剂/氧化剂等应急药剂)。 * * 2. POST /lab-sync/{wwSamplingTaskId} * WwSamplingTask → 真实写入 LabSample(通用实验室样品台账),消除"数据孤岛"。 * 将废水检测任务的样品信息同步到实验室统一 LabSample 体系(sampleType=水样, source=工业废水运营)。 * * 3. POST /finance-cost-link * 运营成本月报 → 真实写入 ArApItem(应付账款),建立财务成本核算真实联动。 * 按账期归集的运营成本自动推送至财务应付系统(电费/药费/污泥处置费汇总)。 * * 4. POST /archive-link * 水质检测报告、月报等运营文件 → 真实写入 ArchiveController(档案室),消除"标记归档"与"真实写入"的差距。 * * 5. GET /lab-sync-status/{wwSamplingTaskId} * 查询某采样任务与 LabSample 的关联状态(是否已同步、关联的 sampleNo)。 */ @RestController @RequestMapping("/api/oa/ww-xdept-bridge") public class WwCrossDeptBridgeController { private final WwSamplingTaskRepository wwTaskRepo; private final LabSampleRepository labSampleRepo; private final AdminProcurementRequestRepository procurementRepo; private final ArApItemRepository arApRepo; private final ArchiveRepository archiveRepo; public WwCrossDeptBridgeController(WwSamplingTaskRepository wwTaskRepo, LabSampleRepository labSampleRepo, AdminProcurementRequestRepository procurementRepo, ArApItemRepository arApRepo, ArchiveRepository archiveRepo) { this.wwTaskRepo = wwTaskRepo; this.labSampleRepo = labSampleRepo; this.procurementRepo = procurementRepo; this.arApRepo = arApRepo; this.archiveRepo = archiveRepo; } // ============================================================ // 1. WwSamplingTask 超标 → 采购申请单(真实写入 AdminProcurementRequest) // ============================================================ public record ProcurementLinkRequest( String chemName, String spec, String unit, Double qty, Double budgetAmount, String purpose, String expectDate, String applicant) { } public record ProcurementLinkResult( Long wwTaskId, String wwSampleCode, String judgeResult, Long procurementId, String procurementCode, String status) { } /** * 将废水检测超标事件联动为采购部真实采购申请单。 * 适用场景:检测结果超标(如COD超标),需紧急补充中和剂/氧化剂/碳源等药剂。 */ @PostMapping("/procurement-link/{wwTaskId}") @Transactional public ApiResp linkProcurement( @PathVariable Long wwTaskId, @RequestBody ProcurementLinkRequest req) { WwSamplingTask task = wwTaskRepo.findById(wwTaskId) .orElseThrow(() -> new NotFoundException("废水采样任务不存在: " + wwTaskId)); if (req.chemName() == null || req.chemName().isBlank()) { throw new ApiException(400, "药剂名称(chemName)不能为空"); } if (req.qty() == null || req.qty() <= 0) { throw new ApiException(400, "采购数量(qty)必须大于0"); } String purpose = req.purpose() != null ? req.purpose() : "废水检测超标应急处置: 样品[" + task.getSampleCode() + "] 检测结果=" + task.getJudgeResult() + " 超标项=" + (task.getExceedItems() != null ? task.getExceedItems() : "见报告"); AdminProcurementRequest pr = new AdminProcurementRequest(); pr.setCode("PR-WW-" + String.format("%04d", procurementRepo.count() + 1)); pr.setDept("工业废水运营中心"); pr.setApplicant(req.applicant() != null ? req.applicant() : task.getSampler()); pr.setSupplyName(req.chemName()); pr.setCategory("药剂"); pr.setSpec(req.spec()); pr.setUnit(req.unit() != null ? req.unit() : "kg"); pr.setQty(req.qty()); pr.setBudgetAmount(req.budgetAmount() != null ? Money.of(req.budgetAmount()) : BigDecimal.ZERO); pr.setPurpose(purpose); pr.setExpectDate(req.expectDate() != null ? req.expectDate() : LocalDate.now().plusDays(3).toString()); pr.setStatus("待审批"); pr.setCreatedAt(Instant.now()); AdminProcurementRequest saved = procurementRepo.save(pr); return ApiResp.ok(new ProcurementLinkResult( task.getId(), task.getSampleCode(), task.getJudgeResult(), saved.getId(), saved.getCode(), saved.getStatus())); } // ============================================================ // 2. WwSamplingTask → LabSample(消除数据孤岛,真实同步) // ============================================================ public record LabSyncResult( Long wwTaskId, String wwSampleCode, Long labSampleId, String labSampleNo, String syncStatus, String message) { } /** * 将废水采样任务同步到通用实验室 LabSample 体系。 * 消除"WwSamplingTask 数据孤岛"——同步后实验室统一台账可见该样品信息, * 并可使用 LabSampleLifecycleController 进行条码/RFID 追踪。 */ @PostMapping("/lab-sync/{wwTaskId}") @Transactional public ApiResp syncToLab(@PathVariable Long wwTaskId) { WwSamplingTask task = wwTaskRepo.findById(wwTaskId) .orElseThrow(() -> new NotFoundException("废水采样任务不存在: " + wwTaskId)); // 构建 LabSample LabSample s = new LabSample(); // sampleNo = ww 前缀 + 原始编号,避免与通用实验室编号冲突 String sampleNo = "WW-" + (task.getSampleCode() != null ? task.getSampleCode() : "TASK-" + task.getId()); s.setSampleNo(sampleNo); s.setSampleName((task.getClientName() != null ? task.getClientName() : "工业废水") + " " + (task.getSampleType() != null ? task.getSampleType() : "水样")); s.setSampleType("水样"); s.setSource("工业废水运营"); s.setReceiveDate(task.getActualTime() != null ? task.getActualTime().substring(0, Math.min(10, task.getActualTime().length())) : task.getPlanDate()); // 组装检测项目列表 List items = new ArrayList<>(); if (task.getCodResult() != null) items.add("COD"); if (task.getAmmoniaResult() != null) items.add("氨氮"); if (task.getTnResult() != null) items.add("总氮"); if (task.getTpResult() != null) items.add("总磷"); if (task.getPhResult() != null) items.add("pH"); if (task.getSsResult() != null) items.add("SS"); if (task.getCr6Result() != null) items.add("六价铬"); if (task.getTotalCrResult() != null) items.add("总铬"); if (task.getCyanideResult() != null) items.add("总氰化物"); if (task.getPhenolResult() != null) items.add("挥发酚"); s.setTestItems(items.isEmpty() ? "COD;氨氮;pH" : String.join(";", items)); s.setStatus(task.getStatus() != null && task.getStatus().contains("已检测") ? "已出报告" : "待检"); s.setAssignee(task.getAnalyst() != null ? task.getAnalyst() : task.getSampler()); s.setForm("液态"); s.setQuantity("500 mL"); s.setStorageCondition("4℃冷藏避光"); s.setProjectName("工业废水采样-" + (task.getSamplePoint() != null ? task.getSamplePoint() : "")); s.setCreatedAt(Instant.now()); LabSample saved = labSampleRepo.save(s); return ApiResp.ok(new LabSyncResult( task.getId(), task.getSampleCode(), saved.getId(), saved.getSampleNo(), "已同步", "WwSamplingTask[" + task.getId() + "] 已同步至 LabSample[" + saved.getId() + "]")); } /** 批量同步:将 status=已检测 且未在 LabSample 中的采样任务全量同步。 */ @PostMapping("/lab-sync-batch") @Transactional public ApiResp> syncBatch() { List tasks = wwTaskRepo.findByStatus("已检测"); List results = new ArrayList<>(); for (WwSamplingTask task : tasks) { try { String expectedNo = "WW-" + (task.getSampleCode() != null ? task.getSampleCode() : "TASK-" + task.getId()); // 避免重复同步 LabSample existing = labSampleRepo.findFirstByBarcode(expectedNo); if (existing != null) { results.add(new LabSyncResult(task.getId(), task.getSampleCode(), existing.getId(), existing.getSampleNo(), "已存在", "跳过重复")); continue; } ApiResp r = syncToLab(task.getId()); if (r.data() != null) results.add(r.data()); } catch (Exception e) { results.add(new LabSyncResult(task.getId(), task.getSampleCode(), null, null, "同步失败", e.getMessage())); } } return ApiResp.ok(results); } // ============================================================ // 3. 运营成本 → ArApItem(财务应付账款真实写入) // ============================================================ public record CostFinanceLinkRequest( String period, String costType, Double amount, String partyName, String dueDate, String remark, String operator) { } public record CostFinanceLinkResult( String period, String costType, Long arApId, String arApCode, BigDecimal amount, String status) { } /** * 运营成本月报→财务部应付账款真实写入。 * costType 枚举:电费 / 药剂费 / 污泥处置费 / 维修费 / 人工费。 * partyName:对应的供应商/电力公司/危废处置单位名称。 */ @PostMapping("/finance-cost-link") @Transactional public ApiResp linkFinanceCost(@RequestBody CostFinanceLinkRequest req) { if (req.period() == null || !req.period().matches("\\d{4}-\\d{2}")) { throw new ApiException(400, "账期(period)须为 YYYY-MM 格式"); } if (req.costType() == null || req.costType().isBlank()) { throw new ApiException(400, "成本类型(costType)不能为空"); } if (req.amount() == null || req.amount() <= 0) { throw new ApiException(400, "成本金额(amount)必须大于0"); } if (req.partyName() == null || req.partyName().isBlank()) { throw new ApiException(400, "往来单位(partyName)不能为空"); } BigDecimal amountBD = Money.of(req.amount()); ArApItem ap = new ArApItem(); ap.setCode("AP-WW-" + req.period().replace("-", "") + "-" + (arApRepo.count() + 1)); ap.setArApType(ArApItem.T_AP); ap.setPartyName(req.partyName()); ap.setRelatedRef("WW运营成本-" + req.period() + "-" + req.costType()); ap.setAmount(amountBD); ap.setWrittenOff(BigDecimal.ZERO); ap.setUnwrittenOff(amountBD); ap.setStatus("未核销"); ap.setDueDate(req.dueDate() != null ? req.dueDate() : LocalDate.parse(req.period() + "-28").plusMonths(1).toString()); ap.setCompanySubject("工业废水运营中心"); ap.setOwner(req.operator() != null ? req.operator() : "运营部"); ap.setRemark("账期[" + req.period() + "] " + req.costType() + (req.remark() != null ? " " + req.remark() : "")); ap.setCreatedAt(Instant.now()); ArApItem saved = arApRepo.save(ap); return ApiResp.ok(new CostFinanceLinkResult( req.period(), req.costType(), saved.getId(), saved.getCode(), saved.getAmount(), saved.getStatus())); } // ============================================================ // 4. 运营文件 → ArchiveController(档案室真实写入) // ============================================================ public record ArchiveLinkRequest( String docType, String title, String period, String fileName, String uploader, String remark) { } public record ArchiveLinkResult( Long archiveId, String category, String title, String archiveDate, String status) { } /** * 将运营文件真实写入档案室 Archive 体系,消除"联动记录标记"差距。 * docType:水质检测报告 / 月度运营报告 / 危废台账 / 排污许可执行报告 / 巡检记录。 * 写入后可在档案室统一检索界面查到(category=运营档案)。 */ @PostMapping("/archive-link") @Transactional public ApiResp linkArchive(@RequestBody ArchiveLinkRequest req) { if (req.title() == null || req.title().isBlank()) { throw new ApiException(400, "档案标题(title)不能为空"); } if (req.docType() == null || req.docType().isBlank()) { throw new ApiException(400, "文件类型(docType)不能为空"); } Archive a = new Archive(); a.setCategory("运营档案"); a.setTitle(req.title()); a.setSourceType("运营"); a.setSourceId(null); a.setFileName(req.fileName() != null ? req.fileName() : req.title() + ".pdf"); a.setFileType("pdf"); a.setFileSize(0L); a.setUploader(req.uploader() != null ? req.uploader() : "运营系统"); a.setArchiveDate(LocalDate.now().toString()); a.setTags("工业废水运营;" + req.docType() + (req.period() != null ? ";" + req.period() : "")); a.setSummary("工业废水运营中心-" + req.docType() + (req.period() != null ? " 账期[" + req.period() + "]" : "") + (req.remark() != null ? " " + req.remark() : "")); a.setAccessLevel("内部"); a.setFileUrl(null); a.setStoredFileId(null); a.setCreatedAt(Instant.now()); Archive saved = archiveRepo.save(a); return ApiResp.ok(new ArchiveLinkResult( saved.getId(), saved.getCategory(), saved.getTitle(), saved.getArchiveDate(), "已归档")); } // ============================================================ // 5. 查询联动状态 // ============================================================ public record BridgeStatus( String module, long totalWwTasks, long labSyncedCount, long procurementLinkedCount, long financeLinkedCount, long archiveLinkedCount) { } /** 整体联动状态汇总。 */ @GetMapping("/status") public ApiResp status() { long totalTasks = wwTaskRepo.count(); // LabSample 中 source=工业废水运营 的计数 long labSynced = labSampleRepo.findAll().stream() .filter(s -> "工业废水运营".equals(s.getSource())) .count(); // 采购申请中 dept=工业废水运营中心 的计数 long procLinked = procurementRepo.findByDeptOrderByIdDesc("工业废水运营中心").size(); // ArAp 中 companySubject=工业废水运营中心 的计数 long finLinked = arApRepo.findAll().stream() .filter(a -> "工业废水运营中心".equals(a.getCompanySubject())) .count(); // 档案中 category=运营档案 且 tags含工业废水运营 的计数 long archLinked = archiveRepo.findByCategory("运营档案").stream() .filter(a -> a.getTags() != null && a.getTags().contains("工业废水运营")) .count(); return ApiResp.ok(new BridgeStatus( "工业废水运营中心", totalTasks, labSynced, procLinked, finLinked, archLinked)); } }