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(@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 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 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 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 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(@PathVariable Long id) { SupervisionProject p = projectRepo.findById(id) .orElseThrow(() -> new NotFoundException("supervision project not found: " + id)); List inspections = inspectionRepo.findBySupervisionProjectIdOrderByIdDesc(id); int openRectify = (int) inspections.stream() .filter(x -> { String rs = x.getRectifyStatus(); return rs != null && !"无需整改".equals(rs) && !"已复核闭环".equals(rs); }) .count(); List 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 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)); } }