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,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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user