package com.kaidi.oa.web; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.common.NotFoundException; import com.kaidi.oa.domain.SupervisionProject; import com.kaidi.oa.domain.SvSequenceInspect; import com.kaidi.oa.repository.SupervisionProjectRepository; import com.kaidi.oa.repository.SvSequenceInspectRepository; 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.time.Instant; import java.time.LocalDate; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * 工程监理部 · 工序验收工单(施工过程质量控制 Gap 3 核心补齐)。 * * 完整工单状态机: * 施工方提交申请(POST /) → 待接单 * 监理接单(POST /{id}/accept)→ 检查中 * 现场检查录入 + 出结论(POST /{id}/conclude)→ 已出结论 * 合格→自动生成验收记录表编号 + status=已生成记录 * 不合格/整改→自动生成整改通知单编号 * * 另提供:GET /compare-analysis(平行检验偏差对比聚合,补 Gap 3 平行检验对比逻辑)。 * * 资源路径 /api/oa/sv-sequence-inspects。 */ @RestController @RequestMapping("/api/oa/sv-sequence-inspects") public class SvSequenceInspectController { private static final List INSPECT_LEVELS = List.of("检验批", "分项工程", "分部工程"); private static final List CONCLUSIONS = List.of("合格", "整改", "不合格"); private final SvSequenceInspectRepository repo; private final SupervisionProjectRepository projectRepo; public SvSequenceInspectController(SvSequenceInspectRepository repo, SupervisionProjectRepository projectRepo) { this.repo = repo; this.projectRepo = projectRepo; } @GetMapping public ApiResp> list( @RequestParam(required = false) Long projectId, @RequestParam(required = false) String status, @RequestParam(required = false) String inspectLevel) { if (projectId != null) { return ApiResp.ok(repo.findBySupervisionProjectIdOrderByIdDesc(projectId)); } if (status != null && !status.isBlank()) { return ApiResp.ok(repo.findByStatus(status)); } if (inspectLevel != null && !inspectLevel.isBlank()) { return ApiResp.ok(repo.findByInspectLevel(inspectLevel)); } return ApiResp.ok(repo.findAll()); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(find(id)); } /** * 平行检验偏差对比分析(Gap 3 补齐)。 * 聚合指定监理项目的验收结论统计:合格数/整改数/不合格数 + 合格率, * 为监理与施工自检比对偏差分析提供数据基础。 */ @GetMapping("/compare-analysis") public ApiResp> compareAnalysis(@RequestParam Long projectId) { if (!projectRepo.existsById(projectId)) { throw new NotFoundException("supervision project not found: " + projectId); } List all = repo.findBySupervisionProjectIdOrderByIdDesc(projectId); long total = all.size(); long qualified = all.stream().filter(r -> "合格".equals(r.getConclusion())).count(); long rectify = all.stream().filter(r -> "整改".equals(r.getConclusion())).count(); long unqualified = all.stream().filter(r -> "不合格".equals(r.getConclusion())).count(); long pending = all.stream().filter(r -> r.getConclusion() == null).count(); double qualifiedRate = total > 0 ? Math.round(qualified * 10000.0 / total) / 100.0 : 0.0; Map result = new LinkedHashMap<>(); result.put("projectId", projectId); result.put("total", total); result.put("qualified", qualified); result.put("rectify", rectify); result.put("unqualified", unqualified); result.put("pending", pending); result.put("qualifiedRate", qualifiedRate); result.put("deviationAlert", unqualified > 0 || (total > 0 && qualifiedRate < 80.0)); return ApiResp.ok(result); } public record ApplyRequest( Long supervisionProjectId, String inspectLevel, String partName, String location, String applyDate, String applicant, String selfCheckRecord) { } /** 施工方提交验收申请,生成工单,状态=待接单。 */ @PostMapping @Transactional public ApiResp apply(@RequestBody ApplyRequest req) { if (req.supervisionProjectId() == null) { throw new ApiException(400, "所属监理项目(supervisionProjectId) 不能为空"); } SupervisionProject parent = projectRepo.findById(req.supervisionProjectId()) .orElseThrow(() -> new NotFoundException("supervision project not found: " + req.supervisionProjectId())); if (req.partName() == null || req.partName().isBlank()) { throw new ApiException(400, "分部/分项名称(partName) 不能为空"); } String level = req.inspectLevel() == null || req.inspectLevel().isBlank() ? "检验批" : req.inspectLevel(); if (!INSPECT_LEVELS.contains(level)) { throw new ApiException(400, "验收层级无效,允许:" + INSPECT_LEVELS); } SvSequenceInspect s = new SvSequenceInspect(); s.setSupervisionProjectId(parent.getId()); s.setProjectCode(parent.getCode()); s.setOrderNo("GX-" + (repo.count() + 1)); s.setInspectLevel(level); s.setPartName(req.partName()); s.setLocation(req.location()); s.setApplyDate(req.applyDate() == null || req.applyDate().isBlank() ? LocalDate.now().toString() : req.applyDate()); s.setApplicant(req.applicant()); s.setSelfCheckRecord(req.selfCheckRecord()); s.setRecordGenerated(false); s.setStatus("待接单"); s.setCreatedAt(Instant.now()); return ApiResp.ok(repo.save(s)); } @PatchMapping("/{id}") public ApiResp update(@PathVariable Long id, @RequestBody ApplyRequest req) { SvSequenceInspect s = find(id); if (!"待接单".equals(s.getStatus())) { throw new ApiException(409, "仅「待接单」的工单可修改申请信息,当前:" + s.getStatus()); } if (req.partName() != null && !req.partName().isBlank()) s.setPartName(req.partName()); if (req.location() != null) s.setLocation(req.location()); if (req.applicant() != null) s.setApplicant(req.applicant()); if (req.selfCheckRecord() != null) s.setSelfCheckRecord(req.selfCheckRecord()); return ApiResp.ok(repo.save(s)); } @DeleteMapping("/{id}") public ApiResp delete(@PathVariable Long id) { SvSequenceInspect s = find(id); if (!"待接单".equals(s.getStatus())) { throw new ApiException(409, "仅「待接单」的工单可撤回删除"); } repo.deleteById(id); return ApiResp.ok(null); } // ---------- 业务动作 ---------- public record AcceptRequest(String inspector, String acceptDate) { } /** * 监理接单:待接单 → 检查中,记录接单监理员和接单日期。 */ @PostMapping("/{id}/accept") @Transactional public ApiResp accept(@PathVariable Long id, @RequestBody AcceptRequest req) { SvSequenceInspect s = find(id); if (!"待接单".equals(s.getStatus())) { throw new ApiException(409, "仅「待接单」状态可接单,当前:" + s.getStatus()); } if (req.inspector() == null || req.inspector().isBlank()) { throw new ApiException(400, "接单监理员(inspector) 不能为空"); } s.setInspector(req.inspector()); s.setAcceptDate(req.acceptDate() == null || req.acceptDate().isBlank() ? LocalDate.now().toString() : req.acceptDate()); s.setStatus("检查中"); return ApiResp.ok(repo.save(s)); } public record ConcludeRequest( String checkDate, String checkContent, String photoRefs, String conclusion, String conclusionRemark) { } /** * 出结论:检查中 → 已出结论。 * 合格:自动生成验收记录表编号(YSJ-GX-工单号),recordGenerated=true,status=已生成记录。 * 整改/不合格:自动生成整改通知单编号(ZG-工单号)。 */ @PostMapping("/{id}/conclude") @Transactional public ApiResp conclude(@PathVariable Long id, @RequestBody ConcludeRequest req) { SvSequenceInspect s = find(id); if (!"检查中".equals(s.getStatus())) { throw new ApiException(409, "仅「检查中」状态可出结论,当前:" + s.getStatus()); } if (req.conclusion() == null || req.conclusion().isBlank()) { throw new ApiException(400, "验收结论(conclusion) 不能为空"); } if (!CONCLUSIONS.contains(req.conclusion())) { throw new ApiException(400, "结论无效,允许:" + CONCLUSIONS); } s.setCheckDate(req.checkDate() == null || req.checkDate().isBlank() ? LocalDate.now().toString() : req.checkDate()); if (req.checkContent() != null) s.setCheckContent(req.checkContent()); if (req.photoRefs() != null) s.setPhotoRefs(req.photoRefs()); s.setConclusion(req.conclusion()); if (req.conclusionRemark() != null) s.setConclusionRemark(req.conclusionRemark()); if ("合格".equals(req.conclusion())) { // 自动生成验收记录表 s.setRecordGenerated(true); s.setRecordNo("YSJ-" + s.getOrderNo()); s.setStatus("已生成记录"); } else { // 整改或不合格:生成整改通知单编号 s.setRectifyNoticeNo("ZG-" + s.getOrderNo()); s.setStatus("已出结论"); } return ApiResp.ok(repo.save(s)); } private SvSequenceInspect find(Long id) { return repo.findById(id) .orElseThrow(() -> new NotFoundException("sv-sequence-inspect not found: " + id)); } }