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,245 @@
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.PatentDisclosure;
import com.kaidi.oa.repository.PatentDisclosureRepository;
import com.kaidi.oa.repository.PatentRepository;
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
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;
/**
* 技术交底书与专利申请决策管理(工程管理中心·专利工法办,需求功能2·专利挖掘与提案)。
*
* 独立建模「技术交底书 → 新颖性/创造性评估 → 是否申请决策」的完整决策流,
* 弥补旧 PatentController 仅记录申请结果、缺失申请前决策依据的缺口(W3 Gap-1)。
*
* 状态机:草稿 → 待评估 → 评估中 → 决定申请 → 已立案(关联Patent) | 决定不申请
*
* 关键端点:
* POST /api/oa/patent-disclosures — 提交技术交底书
* POST /api/oa/patent-disclosures/{id}/submit — 提交待评估
* POST /api/oa/patent-disclosures/{id}/start-assess — 开始评估(专利工程师接单)
* POST /api/oa/patent-disclosures/{id}/decide — 评估后做出申请/不申请决策
* POST /api/oa/patent-disclosures/{id}/file — 已立案(关联 Patent 记录)
*/
@RestController
@RequestMapping("/api/oa/patent-disclosures")
public class PatentDisclosureController {
private final PatentDisclosureRepository disclosureRepo;
private final PatentRepository patentRepo;
public PatentDisclosureController(PatentDisclosureRepository disclosureRepo,
PatentRepository patentRepo) {
this.disclosureRepo = disclosureRepo;
this.patentRepo = patentRepo;
}
@GetMapping
public ApiResp<List<PatentDisclosure>> list(
@RequestParam(required = false) String status,
@RequestParam(required = false) String department,
@RequestParam(required = false) String techDomain) {
List<PatentDisclosure> result;
if (status != null && !status.isBlank()) {
result = disclosureRepo.findByDecisionStatus(status);
} else if (department != null && !department.isBlank()) {
result = disclosureRepo.findByDepartment(department);
} else if (techDomain != null && !techDomain.isBlank()) {
result = disclosureRepo.findByTechDomain(techDomain);
} else {
result = disclosureRepo.findAll();
}
return ApiResp.ok(result);
}
@GetMapping("/{id}")
public ApiResp<PatentDisclosure> get(@PathVariable Long id) {
return ApiResp.ok(find(id));
}
public record CreateDisclosureRequest(
String title, String inventors, String submittedBy, String department,
String submittedDate, String techDomain, String ipcClass,
String techProblem, String techSolution, String techEffect, String innovationType) {
}
/** 提交技术交底书(草稿状态,可编辑)。 */
@PostMapping
public ApiResp<PatentDisclosure> create(@RequestBody CreateDisclosureRequest req) {
if (req.title() == null || req.title().isBlank()) {
throw new ApiException(400, "技术名称不能为空");
}
PatentDisclosure d = new PatentDisclosure();
applyCreate(d, req);
d.setDecisionStatus("草稿");
d.setDisclosureNo(genNo());
d.setCreatedAt(Instant.now());
d.setUpdatedAt(Instant.now());
return ApiResp.ok(disclosureRepo.save(d));
}
/** 编辑交底书(仅草稿/退回状态可编辑)。 */
@PutMapping("/{id}")
@Transactional
public ApiResp<PatentDisclosure> update(@PathVariable Long id,
@RequestBody CreateDisclosureRequest req) {
PatentDisclosure d = find(id);
if (!"草稿".equals(d.getDecisionStatus())) {
throw new ApiException(409, "仅草稿状态可编辑,当前:" + d.getDecisionStatus());
}
applyCreate(d, req);
d.setUpdatedAt(Instant.now());
return ApiResp.ok(disclosureRepo.save(d));
}
/** 提交待评估(草稿 → 待评估)。 */
@PostMapping("/{id}/submit")
@Transactional
public ApiResp<PatentDisclosure> submit(@PathVariable Long id) {
PatentDisclosure d = find(id);
requireStatus(d, "草稿", "submit");
if (d.getTechSolution() == null || d.getTechSolution().isBlank()) {
throw new ApiException(400, "提交前请填写技术方案");
}
d.setDecisionStatus("待评估");
if (d.getSubmittedDate() == null || d.getSubmittedDate().isBlank()) {
d.setSubmittedDate(LocalDate.now().toString());
}
d.setUpdatedAt(Instant.now());
return ApiResp.ok(disclosureRepo.save(d));
}
public record StartAssessRequest(String assessor) {
}
/** 专利工程师接单开始评估(待评估 → 评估中)。 */
@PostMapping("/{id}/start-assess")
@Transactional
public ApiResp<PatentDisclosure> startAssess(@PathVariable Long id,
@RequestBody StartAssessRequest req) {
PatentDisclosure d = find(id);
requireStatus(d, "待评估", "start-assess");
if (req.assessor() == null || req.assessor().isBlank()) {
throw new ApiException(400, "评估人不能为空");
}
d.setDecisionStatus("评估中");
d.setAssessor(req.assessor());
d.setAssessedDate(LocalDate.now().toString());
d.setUpdatedAt(Instant.now());
return ApiResp.ok(disclosureRepo.save(d));
}
public record DecideRequest(boolean apply, String noveltyComment, String inventiveComment,
String rejectReason) {
}
/**
* 评估后做出申请/不申请决策(评估中 → 决定申请 | 决定不申请)。
* apply=true → 决定申请;apply=false → 决定不申请(需填 rejectReason)。
*/
@PostMapping("/{id}/decide")
@Transactional
public ApiResp<PatentDisclosure> decide(@PathVariable Long id,
@RequestBody DecideRequest req) {
PatentDisclosure d = find(id);
requireStatus(d, "评估中", "decide");
d.setNoveltyComment(req.noveltyComment());
d.setInventiveComment(req.inventiveComment());
if (req.apply()) {
d.setDecisionStatus("决定申请");
} else {
if (req.rejectReason() == null || req.rejectReason().isBlank()) {
throw new ApiException(400, "决定不申请时必须填写原因");
}
d.setDecisionStatus("决定不申请");
d.setRejectReason(req.rejectReason());
}
d.setUpdatedAt(Instant.now());
return ApiResp.ok(disclosureRepo.save(d));
}
public record FileRequest(Long patentId) {
}
/**
* 标记已立案并关联 Patent 记录(决定申请 → 已立案)。
* patentId 为提前在 /api/oa/patents 创建好的专利申请记录 id。
*/
@PostMapping("/{id}/file")
@Transactional
public ApiResp<PatentDisclosure> file(@PathVariable Long id,
@RequestBody FileRequest req) {
PatentDisclosure d = find(id);
requireStatus(d, "决定申请", "file");
if (req.patentId() == null) {
throw new ApiException(400, "请先在专利台账创建申请记录,再回填 patentId");
}
if (!patentRepo.existsById(req.patentId())) {
throw new NotFoundException("专利记录不存在:" + req.patentId());
}
d.setDecisionStatus("已立案");
d.setPatentId(req.patentId());
d.setUpdatedAt(Instant.now());
return ApiResp.ok(disclosureRepo.save(d));
}
@DeleteMapping("/{id}")
@Transactional
public ApiResp<Void> delete(@PathVariable Long id) {
PatentDisclosure d = find(id);
if (!"草稿".equals(d.getDecisionStatus())) {
throw new ApiException(409, "仅草稿状态可删除");
}
disclosureRepo.deleteById(id);
return ApiResp.ok(null);
}
// ---------- helpers ----------
private PatentDisclosure find(Long id) {
return disclosureRepo.findById(id)
.orElseThrow(() -> new NotFoundException("技术交底书不存在:" + id));
}
private void requireStatus(PatentDisclosure d, String expected, String action) {
if (!expected.equals(d.getDecisionStatus())) {
throw new ApiException(409, "当前状态「" + d.getDecisionStatus()
+ "」不允许操作「" + action + "」,需要:" + expected);
}
}
private void applyCreate(PatentDisclosure d, CreateDisclosureRequest req) {
if (req.title() != null && !req.title().isBlank()) d.setTitle(req.title());
if (req.inventors() != null) d.setInventors(req.inventors());
if (req.submittedBy() != null) d.setSubmittedBy(req.submittedBy());
if (req.department() != null) d.setDepartment(req.department());
if (req.submittedDate() != null) d.setSubmittedDate(req.submittedDate());
if (req.techDomain() != null) d.setTechDomain(req.techDomain());
if (req.ipcClass() != null) d.setIpcClass(req.ipcClass());
if (req.techProblem() != null) d.setTechProblem(req.techProblem());
if (req.techSolution() != null) d.setTechSolution(req.techSolution());
if (req.techEffect() != null) d.setTechEffect(req.techEffect());
if (req.innovationType() != null) d.setInnovationType(req.innovationType());
}
private static long disclosureSeq = System.currentTimeMillis() % 10000;
private static synchronized String genNo() {
return "TJB-" + LocalDate.now().getYear() + "-" + String.format("%04d", ++disclosureSeq % 10000);
}
}