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( @RequestParam(required = false) String status, @RequestParam(required = false) String department, @RequestParam(required = false) String techDomain) { List 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 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 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 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 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 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 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 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 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); } }