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,241 @@
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.RdTestPlan;
import com.kaidi.oa.repository.RdTestPlanRepository;
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.util.List;
/**
* 创新研发中心 / 产品开发部 · 结构化测试计划(需求模块 5 «测试计划与执行»)。
*
* <p>补审计缺口"缺结构化测试计划实体":提供按功能/性能/可靠性/安规/环境分类编制测试计划、
* 在线记录测试数据、上传测试报告的完整端点。状态机:草稿 → 执行中 → 完成 / 失败。
* 缺陷数 defectCount 回填后可关联到问题管理(issueRef)。
*
* <p>写口默认受 AuthInterceptor default-deny(ADMIN/APPROVER) 保护。
*/
@RestController
@RequestMapping("/api/oa/rd-test-plans")
public class RdTestPlanController {
private static final List<String> VALID_CATEGORIES =
List.of("功能", "性能", "可靠性", "安规", "环境", "其他");
private static final List<String> VALID_STATUSES =
List.of("草稿", "执行中", "完成", "失败");
private final RdTestPlanRepository repo;
public RdTestPlanController(RdTestPlanRepository repo) {
this.repo = repo;
}
@GetMapping
public ApiResp<List<RdTestPlan>> list(
@RequestParam(required = false) Long devProjectId,
@RequestParam(required = false) Long prototypeOrderId,
@RequestParam(required = false) String testCategory,
@RequestParam(required = false) String status) {
if (devProjectId != null && testCategory != null && !testCategory.isBlank()) {
return ApiResp.ok(repo.findByDevProjectIdAndTestCategory(devProjectId, testCategory));
}
if (devProjectId != null && status != null && !status.isBlank()) {
return ApiResp.ok(repo.findByDevProjectIdAndStatus(devProjectId, status));
}
if (devProjectId != null) {
return ApiResp.ok(repo.findByDevProjectId(devProjectId));
}
if (prototypeOrderId != null) {
return ApiResp.ok(repo.findByPrototypeOrderId(prototypeOrderId));
}
if (status != null && !status.isBlank()) {
return ApiResp.ok(repo.findByStatus(status));
}
if (testCategory != null && !testCategory.isBlank()) {
return ApiResp.ok(repo.findByTestCategory(testCategory));
}
return ApiResp.ok(repo.findAll());
}
@GetMapping("/{id}")
public ApiResp<RdTestPlan> get(@PathVariable Long id) {
return ApiResp.ok(find(id));
}
public record TestPlanRequest(
Long devProjectId, Long prototypeOrderId,
String planName, String testCategory,
String testGoal, String testMethod, String passCriteria,
String planStartDate, String planEndDate,
String actualStartDate, String actualEndDate,
String tester, String status,
String testData, String reportRef,
Integer defectCount, String issueRef, String testRemark,
String createdBy) {
}
@PostMapping
public ApiResp<RdTestPlan> create(@RequestBody TestPlanRequest req) {
if (req.planName() == null || req.planName().isBlank()) {
throw new ApiException(400, "测试计划名称(planName) 不能为空");
}
if (req.devProjectId() == null) {
throw new ApiException(400, "所属研发立项(devProjectId) 不能为空");
}
if (req.testCategory() != null && !VALID_CATEGORIES.contains(req.testCategory())) {
throw new ApiException(400, "testCategory 须为: " + String.join("/", VALID_CATEGORIES));
}
RdTestPlan p = new RdTestPlan();
apply(p, req);
p.setStatus(req.status() == null || req.status().isBlank() ? "草稿" : req.status());
p.setCreatedAt(Instant.now());
p.setUpdatedAt(Instant.now());
return ApiResp.ok(repo.save(p));
}
@PatchMapping("/{id}")
@Transactional
public ApiResp<RdTestPlan> update(@PathVariable Long id, @RequestBody TestPlanRequest req) {
RdTestPlan p = find(id);
if ("完成".equals(p.getStatus()) || "失败".equals(p.getStatus())) {
if (!"草稿".equals(req.status()) && !"执行中".equals(req.status())) {
// 允许回退到草稿/执行中修改数据;否则只更新 testData/reportRef/issueRef
if (req.testData() != null) p.setTestData(req.testData());
if (req.reportRef() != null) p.setReportRef(req.reportRef());
if (req.issueRef() != null) p.setIssueRef(req.issueRef());
if (req.defectCount() != null) p.setDefectCount(req.defectCount());
p.setUpdatedAt(Instant.now());
return ApiResp.ok(repo.save(p));
}
}
apply(p, req);
p.setUpdatedAt(Instant.now());
return ApiResp.ok(repo.save(p));
}
// ---------- 状态机 ----------
/** 草稿 → 执行中(开始执行,回填实际开始日期)。 */
@PostMapping("/{id}/start")
@Transactional
public ApiResp<RdTestPlan> start(@PathVariable Long id) {
RdTestPlan p = find(id);
if (!"草稿".equals(p.getStatus())) {
throw new ApiException(409, "仅草稿状态的测试计划可开始执行,当前为「" + p.getStatus() + "");
}
p.setStatus("执行中");
if (p.getActualStartDate() == null || p.getActualStartDate().isBlank()) {
p.setActualStartDate(java.time.LocalDate.now().toString());
}
p.setUpdatedAt(Instant.now());
return ApiResp.ok(repo.save(p));
}
/** 执行中 → 完成(测试通过)。 */
@PostMapping("/{id}/complete")
@Transactional
public ApiResp<RdTestPlan> complete(@PathVariable Long id,
@RequestBody(required = false) CompleteRequest req) {
RdTestPlan p = find(id);
if (!"执行中".equals(p.getStatus())) {
throw new ApiException(409, "仅执行中的测试计划可标记完成,当前为「" + p.getStatus() + "");
}
p.setStatus("完成");
p.setActualEndDate(java.time.LocalDate.now().toString());
if (req != null) {
if (req.testData() != null) p.setTestData(req.testData());
if (req.reportRef() != null) p.setReportRef(req.reportRef());
if (req.defectCount() != null) p.setDefectCount(req.defectCount());
if (req.issueRef() != null) p.setIssueRef(req.issueRef());
}
p.setUpdatedAt(Instant.now());
return ApiResp.ok(repo.save(p));
}
/** 执行中 → 失败(测试不通过,缺陷关联问题管理)。 */
@PostMapping("/{id}/fail")
@Transactional
public ApiResp<RdTestPlan> fail(@PathVariable Long id,
@RequestBody(required = false) CompleteRequest req) {
RdTestPlan p = find(id);
if (!"执行中".equals(p.getStatus())) {
throw new ApiException(409, "仅执行中的测试计划可标记失败,当前为「" + p.getStatus() + "");
}
p.setStatus("失败");
p.setActualEndDate(java.time.LocalDate.now().toString());
if (req != null) {
if (req.testData() != null) p.setTestData(req.testData());
if (req.reportRef() != null) p.setReportRef(req.reportRef());
if (req.defectCount() != null) p.setDefectCount(req.defectCount());
if (req.issueRef() != null) p.setIssueRef(req.issueRef());
}
p.setUpdatedAt(Instant.now());
return ApiResp.ok(repo.save(p));
}
public record CompleteRequest(String testData, String reportRef,
Integer defectCount, String issueRef) {
}
@DeleteMapping("/{id}")
public ApiResp<Void> delete(@PathVariable Long id) {
RdTestPlan p = find(id);
if ("执行中".equals(p.getStatus())) {
throw new ApiException(409, "执行中的测试计划不能直接删除,请先标记完成或失败");
}
repo.deleteById(id);
return ApiResp.ok(null);
}
// ---------- helpers ----------
private RdTestPlan find(Long id) {
return repo.findById(id)
.orElseThrow(() -> new NotFoundException("测试计划不存在:" + id));
}
private void apply(RdTestPlan p, TestPlanRequest req) {
if (req.devProjectId() != null) p.setDevProjectId(req.devProjectId());
if (req.prototypeOrderId() != null) p.setPrototypeOrderId(req.prototypeOrderId());
if (req.planName() != null && !req.planName().isBlank()) p.setPlanName(req.planName());
if (req.testCategory() != null) {
if (!req.testCategory().isBlank() && !VALID_CATEGORIES.contains(req.testCategory())) {
throw new ApiException(400, "testCategory 须为: " + String.join("/", VALID_CATEGORIES));
}
p.setTestCategory(req.testCategory());
}
if (req.testGoal() != null) p.setTestGoal(req.testGoal());
if (req.testMethod() != null) p.setTestMethod(req.testMethod());
if (req.passCriteria() != null) p.setPassCriteria(req.passCriteria());
if (req.planStartDate() != null) p.setPlanStartDate(req.planStartDate());
if (req.planEndDate() != null) p.setPlanEndDate(req.planEndDate());
if (req.actualStartDate() != null) p.setActualStartDate(req.actualStartDate());
if (req.actualEndDate() != null) p.setActualEndDate(req.actualEndDate());
if (req.tester() != null) p.setTester(req.tester());
if (req.status() != null && !req.status().isBlank()) {
if (!VALID_STATUSES.contains(req.status())) {
throw new ApiException(400, "status 须为: " + String.join("/", VALID_STATUSES));
}
p.setStatus(req.status());
}
if (req.testData() != null) p.setTestData(req.testData());
if (req.reportRef() != null) p.setReportRef(req.reportRef());
if (req.defectCount() != null) p.setDefectCount(req.defectCount());
if (req.issueRef() != null) p.setIssueRef(req.issueRef());
if (req.testRemark() != null) p.setTestRemark(req.testRemark());
if (req.createdBy() != null) p.setCreatedBy(req.createdBy());
}
}