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,194 @@
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.CompletionAcceptance;
import com.kaidi.oa.domain.MeasurementPayment;
import com.kaidi.oa.domain.SupervisionInspection;
import com.kaidi.oa.domain.SupervisionProject;
import com.kaidi.oa.repository.CompletionAcceptanceRepository;
import com.kaidi.oa.repository.MeasurementPaymentRepository;
import com.kaidi.oa.repository.SupervisionInspectionRepository;
import com.kaidi.oa.repository.SupervisionProjectRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;
/**
* 工程监理部·监理项目主档。一条 SupervisionProject 代表一个监理合同项下受监工程,
* 监理记录(旁站/巡视/平行检验/监理通知)、计量支付(期次核定/支付证书)、竣工验收挂其下。
*
* 本控制器只承载监理项目本身的 CRUD + 状态流转 + 跨子表汇总驾驶舱({@code /{id}/overview})
* 不覆盖既有 SupervisionLogController(/api/oa/supervision-logs)。新资源走 /api/oa/supervision-projects。
*
* 含监理合同额,写口受 AuthInterceptor default-deny(ADMIN/APPROVER) 保护;
* 合同额/计量金额读侧已登记进 SENSITIVE_READ_PREFIXES(由主代理中央合并前缀)。
*/
@RestController
@RequestMapping("/api/oa/supervision-projects")
public class SupervisionProjectController {
private final SupervisionProjectRepository projectRepo;
private final SupervisionInspectionRepository inspectionRepo;
private final MeasurementPaymentRepository paymentRepo;
private final CompletionAcceptanceRepository acceptanceRepo;
public SupervisionProjectController(SupervisionProjectRepository projectRepo,
SupervisionInspectionRepository inspectionRepo,
MeasurementPaymentRepository paymentRepo,
CompletionAcceptanceRepository acceptanceRepo) {
this.projectRepo = projectRepo;
this.inspectionRepo = inspectionRepo;
this.paymentRepo = paymentRepo;
this.acceptanceRepo = acceptanceRepo;
}
// ---------- 监理项目 CRUD ----------
@GetMapping
public ApiResp<List<SupervisionProject>> list(@RequestParam(required = false) String status,
@RequestParam(required = false) String stage) {
if (status != null && !status.isBlank()) {
return ApiResp.ok(projectRepo.findByStatus(status));
}
if (stage != null && !stage.isBlank()) {
return ApiResp.ok(projectRepo.findByStage(stage));
}
return ApiResp.ok(projectRepo.findAll());
}
@GetMapping("/{id}")
public ApiResp<SupervisionProject> get(@PathVariable Long id) {
return ApiResp.ok(projectRepo.findById(id)
.orElseThrow(() -> new NotFoundException("supervision project not found: " + id)));
}
public record ProjectRequest(
String code, String projectName, Long projectId, String contractNo, Long contractId,
String owner, String constructor, String chiefSupervisor, String supervisionUnit,
String scale, Double contractAmount, String startDate, String endDate,
String stage, String status, String remark) {
}
@PostMapping
public ApiResp<SupervisionProject> create(@RequestBody ProjectRequest req) {
if (req.projectName() == null || req.projectName().isBlank()) {
throw new ApiException(400, "工程名称(projectName) 不能为空");
}
SupervisionProject p = new SupervisionProject();
p.setCode(req.code() == null || req.code().isBlank()
? "JL-" + (projectRepo.count() + 1) : req.code());
p.setProjectName(req.projectName());
p.setProjectId(req.projectId());
p.setContractNo(req.contractNo());
p.setContractId(req.contractId());
p.setOwner(req.owner());
p.setConstructor(req.constructor());
p.setChiefSupervisor(req.chiefSupervisor());
p.setSupervisionUnit(req.supervisionUnit());
p.setScale(req.scale());
p.setContractAmount(Money.of(req.contractAmount()));
p.setStartDate(req.startDate());
p.setEndDate(req.endDate());
p.setStage(req.stage() == null || req.stage().isBlank() ? "施工准备" : req.stage());
p.setStatus(req.status() == null || req.status().isBlank() ? "筹备" : req.status());
p.setRemark(req.remark());
p.setCreatedAt(Instant.now());
return ApiResp.ok(projectRepo.save(p));
}
@PatchMapping("/{id}")
public ApiResp<SupervisionProject> update(@PathVariable Long id, @RequestBody ProjectRequest req) {
SupervisionProject p = projectRepo.findById(id)
.orElseThrow(() -> new NotFoundException("supervision project not found: " + id));
if (req.projectName() != null && !req.projectName().isBlank()) p.setProjectName(req.projectName());
if (req.projectId() != null) p.setProjectId(req.projectId());
if (req.contractNo() != null) p.setContractNo(req.contractNo());
if (req.contractId() != null) p.setContractId(req.contractId());
if (req.owner() != null) p.setOwner(req.owner());
if (req.constructor() != null) p.setConstructor(req.constructor());
if (req.chiefSupervisor() != null) p.setChiefSupervisor(req.chiefSupervisor());
if (req.supervisionUnit() != null) p.setSupervisionUnit(req.supervisionUnit());
if (req.scale() != null) p.setScale(req.scale());
if (req.contractAmount() != null) p.setContractAmount(Money.of(req.contractAmount()));
if (req.startDate() != null) p.setStartDate(req.startDate());
if (req.endDate() != null) p.setEndDate(req.endDate());
if (req.stage() != null && !req.stage().isBlank()) p.setStage(req.stage());
if (req.status() != null && !req.status().isBlank()) p.setStatus(req.status());
if (req.remark() != null) p.setRemark(req.remark());
return ApiResp.ok(projectRepo.save(p));
}
@DeleteMapping("/{id}")
@Transactional
public ApiResp<Void> delete(@PathVariable Long id) {
if (!projectRepo.existsById(id)) {
throw new NotFoundException("supervision project not found: " + id);
}
// 级联清理本监理项目的监理记录 / 计量支付 / 竣工验收,杜绝悬挂子表行。
inspectionRepo.deleteBySupervisionProjectId(id);
paymentRepo.deleteBySupervisionProjectId(id);
acceptanceRepo.deleteBySupervisionProjectId(id);
projectRepo.deleteById(id);
return ApiResp.ok(null);
}
// ---------- 监理驾驶舱(跨子表汇总) ----------
public record Overview(
SupervisionProject project,
int inspectionCount, int openRectifyCount,
int paymentCount, double declaredTotal, double approvedTotal, double certificateTotal,
int acceptanceCount, boolean completed) {
}
/**
* 监理项目驾驶舱:汇总监理记录数 / 未闭环整改数、计量期数与申报/核定/支付证书金额合计、
* 竣工验收次数与是否已通过。给项目卡片/详情抽屉一次取数。
*/
@GetMapping("/{id}/overview")
public ApiResp<Overview> overview(@PathVariable Long id) {
SupervisionProject p = projectRepo.findById(id)
.orElseThrow(() -> new NotFoundException("supervision project not found: " + id));
List<SupervisionInspection> inspections = inspectionRepo.findBySupervisionProjectIdOrderByIdDesc(id);
int openRectify = (int) inspections.stream()
.filter(x -> {
String rs = x.getRectifyStatus();
return rs != null && !"无需整改".equals(rs) && !"已复核闭环".equals(rs);
})
.count();
List<MeasurementPayment> payments = paymentRepo.findBySupervisionProjectIdOrderByPeriodNoAsc(id);
BigDecimal declared = BigDecimal.ZERO;
BigDecimal approved = BigDecimal.ZERO;
BigDecimal certificate = BigDecimal.ZERO;
for (MeasurementPayment mp : payments) {
declared = Money.add(declared, Money.nz(mp.getDeclaredAmount()));
approved = Money.add(approved, Money.nz(mp.getApprovedAmount()));
certificate = Money.add(certificate, Money.nz(mp.getCertificateAmount()));
}
List<CompletionAcceptance> acceptances = acceptanceRepo.findBySupervisionProjectIdOrderByIdDesc(id);
boolean completed = acceptances.stream()
.anyMatch(a -> "正式竣工验收".equals(a.getAcceptType()) && "已通过".equals(a.getStatus()));
return ApiResp.ok(new Overview(p,
inspections.size(), openRectify,
payments.size(), declared.doubleValue(), approved.doubleValue(), certificate.doubleValue(),
acceptances.size(), completed));
}
}