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:
@@ -0,0 +1,170 @@
|
||||
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.LabEln;
|
||||
import com.kaidi.oa.domain.LabInternalAudit;
|
||||
import com.kaidi.oa.domain.TestTask;
|
||||
import com.kaidi.oa.repository.LabElnRepository;
|
||||
import com.kaidi.oa.repository.LabInternalAuditRepository;
|
||||
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.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 实验室·内部审核(质量管理与合规 §6 深化)。按周期自动抽取一定比例实验记录做合规检查并生成审核报告。
|
||||
*
|
||||
* - generate 按 scope/samplePct 从 ELN/检测任务中随机抽样,自动跑机检合规项(签名/复核/原始数据齐全),
|
||||
* 产出初步 findings 与合规率,落「检查中」;
|
||||
* - report 定稿审核报告:补审核员人工结论,置「已出报告」。
|
||||
*
|
||||
* 机检规则示例:ELN 须已签名且数据完整;检测任务须双人复核(reviewer≠tester)且有结果值。
|
||||
* 写口默认受 default-deny(ADMIN/APPROVER) 保护。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/oa/lab-internal-audits")
|
||||
public class LabInternalAuditController {
|
||||
|
||||
private static final List<String> SCOPES = List.of("ELN", "检测任务", "全部");
|
||||
|
||||
private final LabInternalAuditRepository auditRepo;
|
||||
private final LabElnRepository elnRepo;
|
||||
private final TestTaskRepository taskRepo;
|
||||
|
||||
public LabInternalAuditController(LabInternalAuditRepository auditRepo,
|
||||
LabElnRepository elnRepo,
|
||||
TestTaskRepository taskRepo) {
|
||||
this.auditRepo = auditRepo;
|
||||
this.elnRepo = elnRepo;
|
||||
this.taskRepo = taskRepo;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ApiResp<List<LabInternalAudit>> list(@RequestParam(required = false) String status) {
|
||||
if (status != null && !status.isBlank()) {
|
||||
return ApiResp.ok(auditRepo.findByStatus(status));
|
||||
}
|
||||
return ApiResp.ok(auditRepo.findAll());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ApiResp<LabInternalAudit> get(@PathVariable Long id) {
|
||||
return ApiResp.ok(must(id));
|
||||
}
|
||||
|
||||
public record GenerateRequest(String period, String scope, Integer samplePct, String auditor) {
|
||||
}
|
||||
|
||||
/** 抽样并机检:随机抽 samplePct% 的记录,跑合规项,算合规率,落「检查中」。 */
|
||||
@PostMapping("/generate")
|
||||
@Transactional
|
||||
public ApiResp<LabInternalAudit> generate(@RequestBody GenerateRequest req) {
|
||||
String scope = req.scope() == null || req.scope().isBlank() ? "全部" : req.scope();
|
||||
if (!SCOPES.contains(scope)) {
|
||||
throw new ApiException(400, "审核范围只能是 " + SCOPES);
|
||||
}
|
||||
int pct = req.samplePct() == null ? 20 : req.samplePct();
|
||||
if (pct < 1 || pct > 100) {
|
||||
throw new ApiException(400, "抽样比例(samplePct) 必须在 1-100 之间");
|
||||
}
|
||||
|
||||
// 收集候选记录(ref + 机检结果)。
|
||||
List<String> sampledRefs = new ArrayList<>();
|
||||
List<String> findings = new ArrayList<>();
|
||||
int total = 0;
|
||||
int passed = 0;
|
||||
|
||||
if ("ELN".equals(scope) || "全部".equals(scope)) {
|
||||
List<LabEln> elns = new ArrayList<>(elnRepo.findAll());
|
||||
Collections.shuffle(elns);
|
||||
int take = (int) Math.ceil(elns.size() * pct / 100.0);
|
||||
for (int i = 0; i < Math.min(take, elns.size()); i++) {
|
||||
LabEln e = elns.get(i);
|
||||
boolean ok = Boolean.TRUE.equals(e.getSigned())
|
||||
&& e.getRawData() != null && !e.getRawData().isBlank();
|
||||
String result = ok ? "合规" : "不合规";
|
||||
String note = Boolean.TRUE.equals(e.getSigned())
|
||||
? (ok ? "已签名且原始数据完整" : "缺原始数据") : "未电子签名";
|
||||
sampledRefs.add("ELN#" + e.getId() + " " + nz(e.getElnNo()));
|
||||
findings.add("{\"ref\":\"ELN#" + e.getId() + "\",\"item\":\"签名+原始数据\",\"result\":\""
|
||||
+ result + "\",\"note\":\"" + note + "\"}");
|
||||
total++;
|
||||
if (ok) passed++;
|
||||
}
|
||||
}
|
||||
if ("检测任务".equals(scope) || "全部".equals(scope)) {
|
||||
List<TestTask> tasks = new ArrayList<>(taskRepo.findAll());
|
||||
Collections.shuffle(tasks);
|
||||
int take = (int) Math.ceil(tasks.size() * pct / 100.0);
|
||||
for (int i = 0; i < Math.min(take, tasks.size()); i++) {
|
||||
TestTask t = tasks.get(i);
|
||||
boolean hasResult = t.getResultValue() != null && !t.getResultValue().isBlank();
|
||||
boolean dualReview = t.getReviewer() != null && !t.getReviewer().isBlank()
|
||||
&& !t.getReviewer().equals(t.getTester());
|
||||
boolean ok = hasResult && dualReview;
|
||||
String result = ok ? "合规" : "不合规";
|
||||
String note = !hasResult ? "无结果值" : (!dualReview ? "未双人复核" : "结果+双人复核齐全");
|
||||
sampledRefs.add("TASK#" + t.getId() + " " + nz(t.getTaskNo()));
|
||||
findings.add("{\"ref\":\"TASK#" + t.getId() + "\",\"item\":\"结果+双人复核\",\"result\":\""
|
||||
+ result + "\",\"note\":\"" + note + "\"}");
|
||||
total++;
|
||||
if (ok) passed++;
|
||||
}
|
||||
}
|
||||
|
||||
double rate = total == 0 ? 100.0 : Math.round(passed * 10000.0 / total) / 100.0;
|
||||
|
||||
LabInternalAudit a = new LabInternalAudit();
|
||||
a.setAuditNo("IA-" + (auditRepo.count() + 1));
|
||||
a.setPeriod(req.period());
|
||||
a.setScope(scope);
|
||||
a.setSamplePct(pct);
|
||||
a.setSampledCount(total);
|
||||
a.setSampledJson("[\"" + String.join("\",\"", sampledRefs) + "\"]");
|
||||
a.setFindingsJson("[" + String.join(",", findings) + "]");
|
||||
a.setPassRate(rate);
|
||||
a.setAuditor(req.auditor());
|
||||
a.setStatus("检查中");
|
||||
a.setConclusion(total == 0 ? "无可抽样记录" : (rate >= 90 ? "整体合规" : "存在不符合项,需整改"));
|
||||
a.setCreatedAt(Instant.now());
|
||||
return ApiResp.ok(auditRepo.save(a));
|
||||
}
|
||||
|
||||
public record ReportRequest(String auditor, String conclusion) {
|
||||
}
|
||||
|
||||
/** 定稿审核报告:补人工结论,置「已出报告」。 */
|
||||
@PostMapping("/{id}/report")
|
||||
@Transactional
|
||||
public ApiResp<LabInternalAudit> report(@PathVariable Long id, @RequestBody ReportRequest req) {
|
||||
LabInternalAudit a = must(id);
|
||||
if ("已出报告".equals(a.getStatus())) {
|
||||
throw new ApiException(409, "审核报告已定稿");
|
||||
}
|
||||
if (req.auditor() != null && !req.auditor().isBlank()) a.setAuditor(req.auditor());
|
||||
if (req.conclusion() != null && !req.conclusion().isBlank()) a.setConclusion(req.conclusion());
|
||||
a.setStatus("已出报告");
|
||||
return ApiResp.ok(auditRepo.save(a));
|
||||
}
|
||||
|
||||
private LabInternalAudit must(Long id) {
|
||||
return auditRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("internal audit not found: " + id));
|
||||
}
|
||||
|
||||
private static String nz(String v) {
|
||||
return v == null ? "" : v;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user