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:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
@@ -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));
}
}