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>
This commit is contained in:
@@ -0,0 +1,398 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user