Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/LabDeviationController.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

201 lines
8.3 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.LabDeviation;
import com.kaidi.oa.domain.TestTask;
import com.kaidi.oa.repository.LabDeviationRepository;
import com.kaidi.oa.repository.TestTaskRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
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.List;
/**
* 实验室·偏差报告 OOS/OOT 调查闭环(实验任务与检测流程 §2 深化)。
*
* 把 TestTask 命中 OOS偏差 后“仅置态”补深为真正的偏差闭环:
* 登记(open) → investigate 调查(写根因/结论,置「待决策」)→ decide 决策:
* - 复检:基于原任务克隆一张复检 TestTask(解锁结果待重测),偏差置「复检」;
* - 放行:经审核放行,偏差置「已放行」,原任务回到「待复核」;
* - 驳回:偏差置「已驳回」。
*
* 与 TestTask 真关联(taskId),跨实体推进。写口默认受 default-deny(ADMIN/APPROVER) 保护。
*/
@RestController
@RequestMapping("/api/oa/lab-deviations")
public class LabDeviationController {
private static final List<String> TYPES = List.of("OOS", "OOT", "设备异常");
private static final List<String> DECISIONS = List.of("复检", "放行", "驳回");
private final LabDeviationRepository devRepo;
private final TestTaskRepository taskRepo;
public LabDeviationController(LabDeviationRepository devRepo, TestTaskRepository taskRepo) {
this.devRepo = devRepo;
this.taskRepo = taskRepo;
}
@GetMapping
public ApiResp<List<LabDeviation>> list(@RequestParam(required = false) String status,
@RequestParam(required = false) Long taskId) {
if (taskId != null) {
return ApiResp.ok(devRepo.findByTaskId(taskId));
}
if (status != null && !status.isBlank()) {
return ApiResp.ok(devRepo.findByStatus(status));
}
return ApiResp.ok(devRepo.findAll());
}
@GetMapping("/{id}")
public ApiResp<LabDeviation> get(@PathVariable Long id) {
return ApiResp.ok(must(id));
}
public record OpenRequest(Long taskId, String deviationType, String reporter, String description) {
}
/** 登记偏差:从检测任务带出上下文,落「登记」。 */
@PostMapping
@Transactional
public ApiResp<LabDeviation> open(@RequestBody OpenRequest req) {
if (req.taskId() == null) {
throw new ApiException(400, "检测任务(taskId) 不能为空");
}
TestTask t = taskRepo.findById(req.taskId())
.orElseThrow(() -> new ApiException(400, "检测任务不存在: " + req.taskId()));
String type = req.deviationType() == null || req.deviationType().isBlank() ? "OOS" : req.deviationType();
if (!TYPES.contains(type)) {
throw new ApiException(400, "偏差类型只能是 " + TYPES);
}
LabDeviation d = new LabDeviation();
d.setDeviationNo(type + "-" + (devRepo.count() + 1));
d.setTaskId(t.getId());
d.setTaskNo(t.getTaskNo());
d.setSampleName(t.getSampleName());
d.setDetectItem(t.getDetectItem());
d.setDeviationType(type);
d.setResultValue(t.getResultValue());
d.setStandardLimit(t.getStandardLimit());
d.setReporter(req.reporter());
d.setDescription(req.description());
d.setStatus("登记");
d.setCreatedAt(Instant.now());
// 任务进入偏差处理态。
t.setStatus("偏差处理");
taskRepo.save(t);
return ApiResp.ok(devRepo.save(d));
}
public record InvestigateRequest(String investigator, String rootCause, String investigation) {
}
/** 调查:写调查人/根因/结论,置「待决策」。 */
@PostMapping("/{id}/investigate")
@Transactional
public ApiResp<LabDeviation> investigate(@PathVariable Long id, @RequestBody InvestigateRequest req) {
LabDeviation d = must(id);
if ("已放行".equals(d.getStatus()) || "已驳回".equals(d.getStatus())) {
throw new ApiException(409, "偏差已结案,不能再调查");
}
if (req.investigator() == null || req.investigator().isBlank()) {
throw new ApiException(400, "调查人(investigator) 不能为空");
}
d.setInvestigator(req.investigator());
d.setRootCause(req.rootCause());
d.setInvestigation(req.investigation());
d.setStatus("待决策");
return ApiResp.ok(devRepo.save(d));
}
public record DecideRequest(String decision, String approver, String remark) {
}
/** 决策:复检(克隆复检任务)/ 放行 / 驳回。须先完成调查(待决策态)。 */
@PostMapping("/{id}/decide")
@Transactional
public ApiResp<LabDeviation> decide(@PathVariable Long id, @RequestBody DecideRequest req) {
LabDeviation d = must(id);
if (!"待决策".equals(d.getStatus())) {
throw new ApiException(409, "须先完成调查(待决策)才能决策,当前:" + d.getStatus());
}
String dec = req.decision();
if (dec == null || !DECISIONS.contains(dec)) {
throw new ApiException(400, "决策只能是 " + DECISIONS);
}
if (req.approver() == null || req.approver().isBlank()) {
throw new ApiException(400, "决策人(approver) 不能为空");
}
d.setDecision(dec);
d.setApprover(req.approver());
TestTask orig = d.getTaskId() == null ? null : taskRepo.findById(d.getTaskId()).orElse(null);
switch (dec) {
case "复检" -> {
if (orig == null) {
throw new ApiException(409, "原检测任务已不存在,无法复检");
}
TestTask re = cloneForRecheck(orig);
TestTask saved = taskRepo.save(re);
d.setRecheckTaskId(saved.getId());
d.setStatus("复检");
}
case "放行" -> {
if (orig != null) {
// 放行:偏差经审核接受,任务回到待复核继续主流程。
orig.setStatus("待复核");
taskRepo.save(orig);
}
d.setStatus("已放行");
d.setClosedDate(LocalDate.now().toString());
}
default -> { // 驳回
d.setStatus("已驳回");
d.setClosedDate(LocalDate.now().toString());
}
}
return ApiResp.ok(devRepo.save(d));
}
// ---------- helpers ----------
private LabDeviation must(Long id) {
return devRepo.findById(id)
.orElseThrow(() -> new NotFoundException("deviation not found: " + id));
}
/** 克隆原任务为复检任务:保留样品/项目/方法/仪器,清空结果与锁,置「检测中」。 */
private TestTask cloneForRecheck(TestTask src) {
TestTask re = new TestTask();
re.setTaskNo("FJ-" + (taskRepo.count() + 1));
re.setSampleId(src.getSampleId());
re.setSampleNo(src.getSampleNo());
re.setSampleName(src.getSampleName());
re.setDetectItem(src.getDetectItem());
re.setMethodNo(src.getMethodNo());
re.setMethodName(src.getMethodName());
re.setInstrumentId(src.getInstrumentId());
re.setInstrumentName(src.getInstrumentName());
re.setTester(src.getTester());
re.setRelatedTo(src.getRelatedTo());
re.setPriority("高");
re.setStandardLimit(src.getStandardLimit());
re.setResultUnit(src.getResultUnit());
re.setStatus("检测中");
re.setResultLocked(false);
re.setRemark("OOS偏差复检(源任务 " + src.getTaskNo() + "");
re.setCreatedAt(Instant.now());
return re;
}
}