package com.kaidi.oa.web; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.common.NotFoundException; import com.kaidi.oa.domain.MeasurementPayment; import com.kaidi.oa.domain.SupervisionInspection; import com.kaidi.oa.domain.SupervisionProject; import com.kaidi.oa.domain.SvNotice; import com.kaidi.oa.repository.MeasurementPaymentRepository; import com.kaidi.oa.repository.SupervisionInspectionRepository; import com.kaidi.oa.repository.SupervisionProjectRepository; import com.kaidi.oa.repository.SvNoticeRepository; import com.kaidi.oa.repository.SvSurveyVerifyRepository; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; 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.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * 工程监理部 · 审计只读查询端口(Gap 9 补全)。 * * 审计缺口: * ① 未实现专为建设单位/审计机构的只读查询端口(指定时段质量验收/计量支付/整改闭环记录导出)。 * ② 影像资料与具体工序/问题的索引关联仅依赖 imageRefs 字符串字段而非结构化索引。 * * 本控制器提供: * GET /sv-audit-query/projects/{svProjectId}/quality-records?dateFrom=&dateTo= * — 指定时段质量验收记录只读导出(合格/整改/不合格,含整改闭环状态) * GET /sv-audit-query/projects/{svProjectId}/payment-records?dateFrom=&dateTo= * — 指定时段计量支付记录只读导出(已签发支付证书,含金额汇总) * GET /sv-audit-query/projects/{svProjectId}/rectify-records?dateFrom=&dateTo= * — 指定时段整改闭环记录只读导出(问题→整改→复核闭环全链路) * GET /sv-audit-query/projects/{svProjectId}/image-index?type= * — 结构化影像索引查询(基于 SvSurveyVerify.imageIndexJson + SupervisionInspection 记录聚合) * GET /sv-audit-query/projects/{svProjectId}/full-export?dateFrom=&dateTo= * — 全量审计包(含质量/支付/整改/影像索引汇总,供审计机构一键下载) * * 资源路径 /api/oa/sv-audit-query(只读,所有端点均为 GET)。 */ @RestController @RequestMapping("/api/oa/sv-audit-query") public class SvAuditQueryController { private final SupervisionInspectionRepository inspectionRepo; private final MeasurementPaymentRepository paymentRepo; private final SvNoticeRepository noticeRepo; private final SvSurveyVerifyRepository surveyVerifyRepo; private final SupervisionProjectRepository projectRepo; public SvAuditQueryController(SupervisionInspectionRepository inspectionRepo, MeasurementPaymentRepository paymentRepo, SvNoticeRepository noticeRepo, SvSurveyVerifyRepository surveyVerifyRepo, SupervisionProjectRepository projectRepo) { this.inspectionRepo = inspectionRepo; this.paymentRepo = paymentRepo; this.noticeRepo = noticeRepo; this.surveyVerifyRepo = surveyVerifyRepo; this.projectRepo = projectRepo; } // ====== 质量验收记录只读导出 ====== @GetMapping("/projects/{svProjectId}/quality-records") public ApiResp> qualityRecords( @PathVariable Long svProjectId, @RequestParam(required = false) String dateFrom, @RequestParam(required = false) String dateTo) { SupervisionProject svp = findProject(svProjectId); String from = dateFrom != null ? dateFrom : "2020-01-01"; String to = dateTo != null ? dateTo : "2099-12-31"; List records = inspectionRepo.findBySupervisionProjectIdOrderByIdDesc(svProjectId).stream() .filter(i -> i.getInspectDate() != null && i.getInspectDate().compareTo(from) >= 0 && i.getInspectDate().compareTo(to) <= 0) .sorted((a, b) -> a.getInspectDate().compareTo(b.getInspectDate())) .toList(); int total = records.size(); long qualified = records.stream().filter(i -> "合格".equals(i.getConclusion())).count(); long problems = records.stream() .filter(i -> "整改".equals(i.getConclusion()) || "不合格".equals(i.getConclusion()) || "停工".equals(i.getConclusion())).count(); long closed = records.stream().filter(i -> "已复核闭环".equals(i.getRectifyStatus())).count(); List> rows = records.stream().map(i -> { Map row = new LinkedHashMap<>(); row.put("id", i.getId()); row.put("code", i.getCode()); row.put("inspectType", i.getInspectType()); row.put("part", i.getPart()); row.put("inspectDate", i.getInspectDate()); row.put("conclusion", i.getConclusion()); row.put("problem", i.getProblem()); row.put("rectifyStatus", i.getRectifyStatus()); row.put("rectifyDueDate", i.getRectifyDueDate()); row.put("rectifyResult", i.getRectifyResult()); row.put("inspector", i.getInspector()); return row; }).collect(Collectors.toList()); Map result = new LinkedHashMap<>(); result.put("svProjectId", svProjectId); result.put("projectName", svp.getProjectName()); result.put("dateFrom", from); result.put("dateTo", to); result.put("totalCount", total); result.put("qualifiedCount", qualified); result.put("problemCount", problems); result.put("closedCount", closed); result.put("qualifiedRate", total == 0 ? 100.0 : Math.round((double) qualified / total * 10000.0) / 100.0); result.put("closedRate", problems == 0 ? 100.0 : Math.round((double) closed / problems * 10000.0) / 100.0); result.put("records", rows); return ApiResp.ok(result); } // ====== 计量支付记录只读导出 ====== @GetMapping("/projects/{svProjectId}/payment-records") public ApiResp> paymentRecords( @PathVariable Long svProjectId, @RequestParam(required = false) String dateFrom, @RequestParam(required = false) String dateTo) { SupervisionProject svp = findProject(svProjectId); String from = dateFrom != null ? dateFrom : "2020-01-01"; String to = dateTo != null ? dateTo : "2099-12-31"; List payments = paymentRepo.findBySupervisionProjectIdOrderByPeriodNoAsc(svProjectId).stream() .filter(p -> { String d = p.getCertificateDate() != null ? p.getCertificateDate() : p.getApplyDate(); return d != null && d.compareTo(from) >= 0 && d.compareTo(to) <= 0; }) .toList(); BigDecimal totalCert = BigDecimal.ZERO; BigDecimal totalApproved = BigDecimal.ZERO; for (MeasurementPayment p : payments) { if ("已签发".equals(p.getStatus())) { totalCert = totalCert.add(p.getCertificateAmount() != null ? p.getCertificateAmount() : BigDecimal.ZERO); } totalApproved = totalApproved.add(p.getApprovedAmount() != null ? p.getApprovedAmount() : BigDecimal.ZERO); } List> rows = payments.stream().map(p -> { Map row = new LinkedHashMap<>(); row.put("id", p.getId()); row.put("code", p.getCode()); row.put("period", p.getPeriod()); row.put("applyDate", p.getApplyDate()); row.put("declaredAmount", p.getDeclaredAmount()); row.put("approvedAmount", p.getApprovedAmount()); row.put("deduction", p.getDeduction()); row.put("retention", p.getRetention()); row.put("certificateAmount", p.getCertificateAmount()); row.put("certificateDate", p.getCertificateDate()); row.put("status", p.getStatus()); row.put("approver", p.getApprover()); return row; }).collect(Collectors.toList()); Map result = new LinkedHashMap<>(); result.put("svProjectId", svProjectId); result.put("projectName", svp.getProjectName()); result.put("dateFrom", from); result.put("dateTo", to); result.put("totalPeriods", payments.size()); result.put("totalCertificateAmount", totalCert); result.put("totalApprovedAmount", totalApproved); result.put("records", rows); return ApiResp.ok(result); } // ====== 整改闭环记录只读导出 ====== @GetMapping("/projects/{svProjectId}/rectify-records") public ApiResp> rectifyRecords( @PathVariable Long svProjectId, @RequestParam(required = false) String dateFrom, @RequestParam(required = false) String dateTo, @RequestParam(required = false) String rectifyStatus) { SupervisionProject svp = findProject(svProjectId); String from = dateFrom != null ? dateFrom : "2020-01-01"; String to = dateTo != null ? dateTo : "2099-12-31"; List records = inspectionRepo.findBySupervisionProjectIdOrderByIdDesc(svProjectId).stream() .filter(i -> { boolean hasProblem = "整改".equals(i.getConclusion()) || "不合格".equals(i.getConclusion()) || "停工".equals(i.getConclusion()); boolean inRange = i.getInspectDate() != null && i.getInspectDate().compareTo(from) >= 0 && i.getInspectDate().compareTo(to) <= 0; boolean statusMatch = rectifyStatus == null || rectifyStatus.isBlank() || rectifyStatus.equals(i.getRectifyStatus()); return hasProblem && inRange && statusMatch; }) .sorted((a, b) -> a.getInspectDate().compareTo(b.getInspectDate())) .toList(); // 关联监理通知单(按 inspectionId 匹配或 code 匹配) List allNotices = noticeRepo.findBySupervisionProjectIdOrderByIdDesc(svProjectId); List> rows = records.stream().map(i -> { Map row = new LinkedHashMap<>(); row.put("id", i.getId()); row.put("code", i.getCode()); row.put("inspectType", i.getInspectType()); row.put("part", i.getPart()); row.put("inspectDate", i.getInspectDate()); row.put("conclusion", i.getConclusion()); row.put("problem", i.getProblem()); row.put("rectifyStatus", i.getRectifyStatus()); row.put("rectifyDueDate", i.getRectifyDueDate()); row.put("rectifyResult", i.getRectifyResult()); row.put("inspector", i.getInspector()); // 关联通知单 List linkedNotices = allNotices.stream() .filter(n -> i.getId().equals(n.getInspectionId())) .map(n -> n.getCode() + "(" + n.getNoticeType() + ")") .collect(Collectors.toList()); row.put("linkedNotices", linkedNotices); return row; }).collect(Collectors.toList()); long closed = records.stream().filter(i -> "已复核闭环".equals(i.getRectifyStatus())).count(); Map result = new LinkedHashMap<>(); result.put("svProjectId", svProjectId); result.put("projectName", svp.getProjectName()); result.put("dateFrom", from); result.put("dateTo", to); result.put("totalProblems", records.size()); result.put("closedCount", closed); result.put("closedRate", records.isEmpty() ? 100.0 : Math.round((double) closed / records.size() * 10000.0) / 100.0); result.put("records", rows); return ApiResp.ok(result); } // ====== 结构化影像索引查询 ====== @GetMapping("/projects/{svProjectId}/image-index") public ApiResp> imageIndex( @PathVariable Long svProjectId, @RequestParam(required = false) String recordType) { SupervisionProject svp = findProject(svProjectId); List> rows = new ArrayList<>(); // 1. 来源:SvSurveyVerify(测量放线复核影像,结构化 imageIndexJson) if (recordType == null || "测量放线".equals(recordType)) { surveyVerifyRepo.findBySupervisionProjectIdOrderByIdDesc(svProjectId).stream() .filter(v -> v.getImageIndexJson() != null && !v.getImageIndexJson().isBlank()) .forEach(v -> { Map row = new LinkedHashMap<>(); row.put("recordType", "测量放线复核"); row.put("recordId", v.getId()); row.put("recordCode", v.getVerifyCode()); row.put("recordDate", v.getVerifyDate()); row.put("part", v.getLocation()); row.put("imageIndexJson", v.getImageIndexJson()); row.put("status", v.getStatus()); row.put("result", v.getResult()); rows.add(row); }); } // 2. 来源:SupervisionInspection(旁站/巡视验收,按 inspectType 聚合影像引用) if (recordType == null || "验收巡视".equals(recordType)) { inspectionRepo.findBySupervisionProjectIdOrderByIdDesc(svProjectId).stream() .filter(i -> i.getContent() != null && i.getContent().contains("http")) .forEach(i -> { Map row = new LinkedHashMap<>(); row.put("recordType", i.getInspectType()); row.put("recordId", i.getId()); row.put("recordCode", i.getCode()); row.put("recordDate", i.getInspectDate()); row.put("part", i.getPart()); row.put("imageIndexJson", null); row.put("contentRef", i.getContent()); row.put("conclusion", i.getConclusion()); rows.add(row); }); } Map result = new LinkedHashMap<>(); result.put("svProjectId", svProjectId); result.put("projectName", svp.getProjectName()); result.put("recordType", recordType != null ? recordType : "全部"); result.put("totalImages", rows.size()); result.put("records", rows); return ApiResp.ok(result); } // ====== 全量审计包 ====== @GetMapping("/projects/{svProjectId}/full-export") public ApiResp> fullExport( @PathVariable Long svProjectId, @RequestParam(required = false) String dateFrom, @RequestParam(required = false) String dateTo) { SupervisionProject svp = findProject(svProjectId); String from = dateFrom != null ? dateFrom : "2020-01-01"; String to = dateTo != null ? dateTo : "2099-12-31"; // 质量验收摘要 List inspections = inspectionRepo.findBySupervisionProjectIdOrderByIdDesc(svProjectId).stream() .filter(i -> i.getInspectDate() != null && i.getInspectDate().compareTo(from) >= 0 && i.getInspectDate().compareTo(to) <= 0) .toList(); long qualified = inspections.stream().filter(i -> "合格".equals(i.getConclusion())).count(); long problems = inspections.stream() .filter(i -> "整改".equals(i.getConclusion()) || "不合格".equals(i.getConclusion()) || "停工".equals(i.getConclusion())).count(); long closedI = inspections.stream().filter(i -> "已复核闭环".equals(i.getRectifyStatus())).count(); // 计量支付摘要 List payments = paymentRepo.findBySupervisionProjectIdOrderByPeriodNoAsc(svProjectId).stream() .filter(p -> { String d = p.getCertificateDate() != null ? p.getCertificateDate() : p.getApplyDate(); return d != null && d.compareTo(from) >= 0 && d.compareTo(to) <= 0; }) .toList(); BigDecimal totalCert = payments.stream() .filter(p -> "已签发".equals(p.getStatus())) .map(p -> p.getCertificateAmount() != null ? p.getCertificateAmount() : BigDecimal.ZERO) .reduce(BigDecimal.ZERO, BigDecimal::add); // 监理通知单摘要 List notices = noticeRepo.findBySupervisionProjectIdOrderByIdDesc(svProjectId).stream() .filter(n -> n.getIssueDate() != null && n.getIssueDate().compareTo(from) >= 0 && n.getIssueDate().compareTo(to) <= 0) .toList(); long closedN = notices.stream().filter(n -> "已闭环".equals(n.getStatus())).count(); // 影像索引摘要 long imageCount = surveyVerifyRepo.findBySupervisionProjectIdOrderByIdDesc(svProjectId).stream() .filter(v -> v.getImageIndexJson() != null && !v.getImageIndexJson().isBlank()) .count(); Map result = new LinkedHashMap<>(); result.put("svProjectId", svProjectId); result.put("projectName", svp.getProjectName()); result.put("projectCode", svp.getCode()); result.put("owner", svp.getOwner()); result.put("chiefSupervisor", svp.getChiefSupervisor()); result.put("dateFrom", from); result.put("dateTo", to); result.put("exportTime", java.time.Instant.now().toString()); Map qualSummary = new LinkedHashMap<>(); qualSummary.put("total", inspections.size()); qualSummary.put("qualified", qualified); qualSummary.put("problems", problems); qualSummary.put("closedProblems", closedI); qualSummary.put("qualifiedRate", inspections.isEmpty() ? 100.0 : Math.round((double) qualified / inspections.size() * 10000.0) / 100.0); qualSummary.put("closedRate", problems == 0 ? 100.0 : Math.round((double) closedI / problems * 10000.0) / 100.0); result.put("qualityAcceptance", qualSummary); Map paymentSummary = new LinkedHashMap<>(); paymentSummary.put("totalPeriods", payments.size()); paymentSummary.put("totalCertificateAmount", totalCert); result.put("measurementPayment", paymentSummary); Map noticeSummary = new LinkedHashMap<>(); noticeSummary.put("total", notices.size()); noticeSummary.put("closed", closedN); noticeSummary.put("closedRate", notices.isEmpty() ? 100.0 : Math.round((double) closedN / notices.size() * 10000.0) / 100.0); result.put("notices", noticeSummary); result.put("imageIndexCount", imageCount); return ApiResp.ok(result); } private SupervisionProject findProject(Long id) { return projectRepo.findById(id) .orElseThrow(() -> new NotFoundException("监理项目不存在:" + id)); } }