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:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
@@ -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;
}
}