Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/SvSequenceInspectController.java
T
QiufengandClaude Opus 4.8 5e51dc3f56 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>
2026-06-15 19:19:15 +08:00

244 lines
11 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<String> INSPECT_LEVELS = List.of("检验批", "分项工程", "分部工程");
private static final List<String> 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<SvSequenceInspect>> 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<SvSequenceInspect> get(@PathVariable Long id) {
return ApiResp.ok(find(id));
}
/**
* 平行检验偏差对比分析(Gap 3 补齐)。
* 聚合指定监理项目的验收结论统计:合格数/整改数/不合格数 + 合格率,
* 为监理与施工自检比对偏差分析提供数据基础。
*/
@GetMapping("/compare-analysis")
public ApiResp<Map<String, Object>> compareAnalysis(@RequestParam Long projectId) {
if (!projectRepo.existsById(projectId)) {
throw new NotFoundException("supervision project not found: " + projectId);
}
List<SvSequenceInspect> 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<String, Object> 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<SvSequenceInspect> 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<SvSequenceInspect> 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<Void> 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<SvSequenceInspect> 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=truestatus=已生成记录。
* 整改/不合格:自动生成整改通知单编号(ZG-工单号)。
*/
@PostMapping("/{id}/conclude")
@Transactional
public ApiResp<SvSequenceInspect> 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));
}
}