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

399 lines
20 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.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<Map<String, Object>> 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<SupervisionInspection> 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<Map<String, Object>> rows = records.stream().map(i -> {
Map<String, Object> 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<String, Object> 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<Map<String, Object>> 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<MeasurementPayment> 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<Map<String, Object>> rows = payments.stream().map(p -> {
Map<String, Object> 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<String, Object> 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<Map<String, Object>> 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<SupervisionInspection> 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<SvNotice> allNotices = noticeRepo.findBySupervisionProjectIdOrderByIdDesc(svProjectId);
List<Map<String, Object>> rows = records.stream().map(i -> {
Map<String, Object> 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<String> 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<String, Object> 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<Map<String, Object>> imageIndex(
@PathVariable Long svProjectId,
@RequestParam(required = false) String recordType) {
SupervisionProject svp = findProject(svProjectId);
List<Map<String, Object>> 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<String, Object> 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<String, Object> 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<String, Object> 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<Map<String, Object>> 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<SupervisionInspection> 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<MeasurementPayment> 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<SvNotice> 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<String, Object> 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<String, Object> 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<String, Object> paymentSummary = new LinkedHashMap<>();
paymentSummary.put("totalPeriods", payments.size());
paymentSummary.put("totalCertificateAmount", totalCert);
result.put("measurementPayment", paymentSummary);
Map<String, Object> 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));
}
}