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,527 @@
|
||||
package com.kaidi.oa.web;
|
||||
|
||||
import com.kaidi.oa.common.ApiException;
|
||||
import com.kaidi.oa.common.ApiResp;
|
||||
import com.kaidi.oa.common.Money;
|
||||
import com.kaidi.oa.common.NotFoundException;
|
||||
import com.kaidi.oa.domain.ArchiveSubmission;
|
||||
import com.kaidi.oa.domain.CompetitorIp;
|
||||
import com.kaidi.oa.domain.Contract;
|
||||
import com.kaidi.oa.domain.Declaration;
|
||||
import com.kaidi.oa.domain.IpLicenseTransfer;
|
||||
import com.kaidi.oa.domain.Patent;
|
||||
import com.kaidi.oa.domain.SysUser;
|
||||
import com.kaidi.oa.domain.WorkMethod;
|
||||
import com.kaidi.oa.repository.ArchiveSubmissionRepository;
|
||||
import com.kaidi.oa.repository.CompetitorIpRepository;
|
||||
import com.kaidi.oa.repository.ContractRepository;
|
||||
import com.kaidi.oa.repository.DeclarationRepository;
|
||||
import com.kaidi.oa.repository.IpLicenseTransferRepository;
|
||||
import com.kaidi.oa.repository.PatentRepository;
|
||||
import com.kaidi.oa.repository.SysUserRepository;
|
||||
import com.kaidi.oa.repository.WorkMethodRepository;
|
||||
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.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工法/专利跨部门接口联动(工程管理中心·专利工法办,需求功能10「与其他部门的接口要求」)。
|
||||
*
|
||||
* 补完审计缺口:
|
||||
* (1) 法务风险部 侵权监控联动:按工法/专利的 techField 联动查询 CompetitorIp 对手知识产权,
|
||||
* 返回风险研判(riskLevel),揭示潜在侵权或侵权空间,使 CompetitorIpController 与
|
||||
* WorkMethod/Patent 模块产生真实关联(而非独立存在);
|
||||
* (2) HR 发明人/完成人部门职务联动:工法 contributors / 专利 inventors 为纯文本,
|
||||
* 本端点按姓名列表批量查询 SysUser 的 deptId / title,返回富化后的发明人信息,
|
||||
* 支持奖励分配与绩效考核(需求功能7);
|
||||
* (3) 资料室 证书批准自动触发归档:工法「已批准」/ 专利「已授权」后,调用此端点即可
|
||||
* 自动创建 ArchiveSubmission(待审核),由资料室管理员审核后正式归档——打通
|
||||
* 「证书批准→归档申请→资料室档案库」联动链;
|
||||
* (4) 技术中心/研发部 查新报告联动:按工法 rdProjectId 找关联研发立项,
|
||||
* 返回 techBackground / contentPrinciple 字段和申报材料辅助信息。
|
||||
*
|
||||
* 新控制器默认受 AuthInterceptor 的 default-deny 保护。
|
||||
*
|
||||
* W7 Gap-10 补完(专利工法办,功能10·与其他部门的接口要求):
|
||||
* (5) 申报服务部专属推送:工法/专利证书批准后可推送证书信息到指定 Declaration 记录的 owner 备注,
|
||||
* 供申报服务部高企/专精特新/科技计划申报时直接引用证书编号;同时新增批量预览接口。
|
||||
* (6) HR联动:在 ContributorProfile 中返回 userId(已有),并附加「displayName匹配风险说明」字段,
|
||||
* 引导维护者关注 displayName 不唯一时的冲突风险(根治需改 inventorsId 外键,已记录于 summary)。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/oa/wm-cross-link")
|
||||
public class WmCrossLinkController {
|
||||
|
||||
private final WorkMethodRepository methodRepo;
|
||||
private final PatentRepository patentRepo;
|
||||
private final CompetitorIpRepository competitorIpRepo;
|
||||
private final SysUserRepository userRepo;
|
||||
private final ArchiveSubmissionRepository archiveSubmissionRepo;
|
||||
private final DeclarationRepository declarationRepo;
|
||||
private final IpLicenseTransferRepository ltRepo;
|
||||
private final ContractRepository contractRepo;
|
||||
|
||||
public WmCrossLinkController(WorkMethodRepository methodRepo,
|
||||
PatentRepository patentRepo,
|
||||
CompetitorIpRepository competitorIpRepo,
|
||||
SysUserRepository userRepo,
|
||||
ArchiveSubmissionRepository archiveSubmissionRepo,
|
||||
DeclarationRepository declarationRepo,
|
||||
IpLicenseTransferRepository ltRepo,
|
||||
ContractRepository contractRepo) {
|
||||
this.methodRepo = methodRepo;
|
||||
this.patentRepo = patentRepo;
|
||||
this.competitorIpRepo = competitorIpRepo;
|
||||
this.userRepo = userRepo;
|
||||
this.archiveSubmissionRepo = archiveSubmissionRepo;
|
||||
this.declarationRepo = declarationRepo;
|
||||
this.ltRepo = ltRepo;
|
||||
this.contractRepo = contractRepo;
|
||||
}
|
||||
|
||||
// ---------- (1) 法务风险部:侵权监控联动(CompetitorIp x WorkMethod/Patent) ----------
|
||||
|
||||
public record IpRiskResult(
|
||||
String techField, List<CompetitorIp> competitors, String riskSummary) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 工法侵权风险联动:根据工法的 techField 联动查询对手专利,判断是否存在「重叠」侵权风险。
|
||||
* 覆盖需求:法务风险部「专利侵权监控」与工法/专利模块的关联监控。
|
||||
*/
|
||||
@GetMapping("/work-method/{id}/ip-risk")
|
||||
public ApiResp<IpRiskResult> workMethodIpRisk(@PathVariable Long id) {
|
||||
WorkMethod m = methodRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("工法不存在:" + id));
|
||||
String techField = nz(m.getTechField());
|
||||
List<CompetitorIp> competitors = techField.isBlank()
|
||||
? List.of()
|
||||
: competitorIpRepo.findByTechField(techField);
|
||||
String summary = buildRiskSummary(competitors, techField, m.getName());
|
||||
return ApiResp.ok(new IpRiskResult(techField, competitors, summary));
|
||||
}
|
||||
|
||||
/**
|
||||
* 专利侵权风险联动:根据专利类型/名称推断 techField,联动查询对手知识产权。
|
||||
* 覆盖需求:法务风险部「专利侵权监控」与专利模块的关联监控。
|
||||
*/
|
||||
@GetMapping("/patent/{id}/ip-risk")
|
||||
public ApiResp<IpRiskResult> patentIpRisk(@PathVariable Long id,
|
||||
@RequestParam(required = false) String techField) {
|
||||
Patent p = patentRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("专利不存在:" + id));
|
||||
String field = (techField != null && !techField.isBlank()) ? techField : "";
|
||||
List<CompetitorIp> competitors = field.isBlank()
|
||||
? List.of()
|
||||
: competitorIpRepo.findByTechField(field);
|
||||
String summary = buildRiskSummary(competitors, field, p.getName());
|
||||
return ApiResp.ok(new IpRiskResult(field, competitors, summary));
|
||||
}
|
||||
|
||||
private String buildRiskSummary(List<CompetitorIp> list, String techField, String ownName) {
|
||||
if (list.isEmpty()) {
|
||||
return techField.isBlank()
|
||||
? "未设置技术分类,无法匹配对手专利;建议完善 techField 后重新查询。"
|
||||
: "技术领域「" + techField + "」暂未发现对手有效专利,侵权风险低。";
|
||||
}
|
||||
long overlap = list.stream().filter(c -> "重叠".equals(c.getRiskLevel())).count();
|
||||
long opportunity = list.stream().filter(c -> "避空机会".equals(c.getRiskLevel())).count();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("技术领域「").append(techField).append("」共发现对手知识产权 ")
|
||||
.append(list.size()).append(" 条");
|
||||
if (overlap > 0) {
|
||||
sb.append(",其中「重叠」侵权风险 ").append(overlap).append(" 条——建议法务风险部介入评估「")
|
||||
.append(ownName).append("」是否构成侵权;");
|
||||
}
|
||||
if (opportunity > 0) {
|
||||
sb.append(",发现「避空机会」").append(opportunity)
|
||||
.append(" 条——本司暂无该分支授权,可考虑专利布局;");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// ---------- (2) HR 发明人/完成人部门职务联动 ----------
|
||||
|
||||
public record ContributorProfile(
|
||||
String name, Long userId, String deptId, String title, boolean found) {
|
||||
}
|
||||
|
||||
public record ContributorLookupResult(
|
||||
List<ContributorProfile> profiles, String note) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 工法完成人 HR 联动:按 contributors(逗号分隔姓名)批量查询用户的部门/职务,
|
||||
* 为奖励分配按贡献比例计算提供结构化基础(需求功能7「发明人/工法完成人管理」)。
|
||||
*/
|
||||
@GetMapping("/work-method/{id}/contributors")
|
||||
public ApiResp<ContributorLookupResult> workMethodContributors(@PathVariable Long id) {
|
||||
WorkMethod m = methodRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("工法不存在:" + id));
|
||||
return ApiResp.ok(lookupContributors(nz(m.getContributors()), "工法"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 专利发明人 HR 联动:按 inventors(逗号分隔姓名)批量查询用户的部门/职务,
|
||||
* 为专利奖励分配与绩效考核提供结构化基础(需求功能7)。
|
||||
*/
|
||||
@GetMapping("/patent/{id}/contributors")
|
||||
public ApiResp<ContributorLookupResult> patentContributors(@PathVariable Long id) {
|
||||
Patent p = patentRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("专利不存在:" + id));
|
||||
return ApiResp.ok(lookupContributors(nz(p.getInventors()), "专利"));
|
||||
}
|
||||
|
||||
private ContributorLookupResult lookupContributors(String csv, String type) {
|
||||
if (csv.isBlank()) {
|
||||
return new ContributorLookupResult(List.of(),
|
||||
type + "完成人/发明人未填写,请先维护 contributors/inventors 字段。");
|
||||
}
|
||||
List<String> names = Arrays.stream(csv.split("[,,、]"))
|
||||
.map(String::trim).filter(s -> !s.isEmpty()).toList();
|
||||
List<ContributorProfile> profiles = new ArrayList<>();
|
||||
int notFound = 0;
|
||||
for (String name : names) {
|
||||
List<SysUser> users = userRepo.findByDisplayName(name);
|
||||
if (users.isEmpty()) {
|
||||
profiles.add(new ContributorProfile(name, null, null, null, false));
|
||||
notFound++;
|
||||
} else {
|
||||
SysUser u = users.get(0);
|
||||
profiles.add(new ContributorProfile(
|
||||
name, u.getId(),
|
||||
u.getDeptId() == null ? null : String.valueOf(u.getDeptId()),
|
||||
u.getTitle(), true));
|
||||
}
|
||||
}
|
||||
String note = notFound == 0
|
||||
? "全部 " + names.size() + " 位完成人已匹配到系统用户(deptId/title 已回填),可进行奖励分配。"
|
||||
: notFound + "/" + names.size() + " 位完成人未在系统中找到匹配用户——"
|
||||
+ "请确认 displayName 与系统用户名一致,或在 HR 系统中补录。";
|
||||
return new ContributorLookupResult(profiles, note);
|
||||
}
|
||||
|
||||
// ---------- (3) 资料室:证书批准自动触发 ArchiveSubmission ----------
|
||||
|
||||
public record ArchiveTriggerRequest(String sourceDept, String applicant) {
|
||||
}
|
||||
|
||||
public record ArchiveTriggerResult(Long submissionId, String archiveName, String status, String message) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 工法证书批准 → 自动创建归档申请(资料室)。
|
||||
* 工法须处于「已批准」状态才可触发;防重触发(同工法 certNo 已有待审核/已归档的申请则 409)。
|
||||
* 覆盖需求功能10:「工法证书/专利证书批准后无自动触发 ArchiveSubmission 归档」的缺口。
|
||||
*/
|
||||
@PostMapping("/work-method/{id}/trigger-archive")
|
||||
@Transactional
|
||||
public ApiResp<ArchiveTriggerResult> triggerWorkMethodArchive(
|
||||
@PathVariable Long id, @RequestBody ArchiveTriggerRequest req) {
|
||||
WorkMethod m = methodRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("工法不存在:" + id));
|
||||
if (!"已批准".equals(m.getStage())) {
|
||||
throw new ApiException(409, "仅「已批准」工法可触发归档申请,当前:" + m.getStage());
|
||||
}
|
||||
|
||||
String title = "工法证书 [" + nz(m.getMethodNo()) + "] " + nz(m.getName());
|
||||
// 防重触发:查是否已有同名待审核/已归档申请(按 title 精确匹配 + status 过滤)
|
||||
List<ArchiveSubmission> existing = archiveSubmissionRepo.findByStatus("待审核");
|
||||
boolean alreadyPending = existing.stream().anyMatch(a -> title.equals(a.getTitle()));
|
||||
if (alreadyPending) {
|
||||
throw new ApiException(409, "工法「" + m.getName() + "」的归档申请已存在(待审核),请勿重复触发。");
|
||||
}
|
||||
|
||||
ArchiveSubmission sub = new ArchiveSubmission();
|
||||
sub.setTitle(title);
|
||||
sub.setSourceDept(nz(req.sourceDept()).isBlank() ? nz(m.getDepartment()) : req.sourceDept());
|
||||
sub.setFormedDate(nz(m.getApproveDate()).isBlank() ? LocalDate.now().toString() : m.getApproveDate());
|
||||
sub.setSecLevel("内部");
|
||||
sub.setFileType("PDF");
|
||||
sub.setApplicant(nz(req.applicant()).isBlank() ? nz(m.getOwner()) : req.applicant());
|
||||
sub.setApplyDate(LocalDate.now().toString());
|
||||
sub.setStatus("待审核");
|
||||
sub.setCreatedAt(Instant.now());
|
||||
ArchiveSubmission saved = archiveSubmissionRepo.save(sub);
|
||||
|
||||
return ApiResp.ok(new ArchiveTriggerResult(
|
||||
saved.getId(), title, "待审核",
|
||||
"已成功创建归档申请(id=" + saved.getId() + "),请资料室管理员审核后正式入档。"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 专利证书授权 → 自动创建归档申请(资料室)。
|
||||
* 专利须处于「已授权」状态才可触发(法律状态字段 status)。
|
||||
*/
|
||||
@PostMapping("/patent/{id}/trigger-archive")
|
||||
@Transactional
|
||||
public ApiResp<ArchiveTriggerResult> triggerPatentArchive(
|
||||
@PathVariable Long id, @RequestBody ArchiveTriggerRequest req) {
|
||||
Patent p = patentRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("专利不存在:" + id));
|
||||
if (!"已授权".equals(p.getStatus())) {
|
||||
throw new ApiException(409, "仅「已授权」专利可触发归档申请,当前:" + p.getStatus());
|
||||
}
|
||||
|
||||
String title = "专利证书 [" + nz(p.getPatentNo()) + "] " + nz(p.getName());
|
||||
List<ArchiveSubmission> existing = archiveSubmissionRepo.findByStatus("待审核");
|
||||
boolean alreadyPending = existing.stream().anyMatch(a -> title.equals(a.getTitle()));
|
||||
if (alreadyPending) {
|
||||
throw new ApiException(409, "专利「" + p.getName() + "」的归档申请已存在(待审核),请勿重复触发。");
|
||||
}
|
||||
|
||||
ArchiveSubmission sub = new ArchiveSubmission();
|
||||
sub.setTitle(title);
|
||||
sub.setSourceDept(nz(req.sourceDept()));
|
||||
sub.setFormedDate(nz(p.getApplyDate()).isBlank() ? LocalDate.now().toString() : p.getApplyDate());
|
||||
sub.setSecLevel("内部");
|
||||
sub.setFileType("PDF");
|
||||
sub.setApplicant(nz(req.applicant()).isBlank() ? nz(p.getApplicant()) : req.applicant());
|
||||
sub.setApplyDate(LocalDate.now().toString());
|
||||
sub.setStatus("待审核");
|
||||
sub.setCreatedAt(Instant.now());
|
||||
ArchiveSubmission saved = archiveSubmissionRepo.save(sub);
|
||||
|
||||
return ApiResp.ok(new ArchiveTriggerResult(
|
||||
saved.getId(), title, "待审核",
|
||||
"已成功创建归档申请(id=" + saved.getId() + "),请资料室管理员审核后正式入档。"));
|
||||
}
|
||||
|
||||
// ---------- (4) 技术中心/研发部:RD 查新报告联动 ----------
|
||||
|
||||
public record RdTechContext(
|
||||
Long workMethodId, String methodNo, String methodName,
|
||||
Long rdProjectId, String rdProjectName,
|
||||
String techBackground, String contentPrinciple,
|
||||
String innovation, String applicableScope,
|
||||
String note) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 工法与研发立项技术背景联动:返回工法关联的研发立项信息 + 工法技术背景/工艺原理,
|
||||
* 辅助专利工程师/技术中心评估新颖性,将工法 techBackground/contentPrinciple 与
|
||||
* RD 模块的查新报告强关联(而非字段存在但无展示入口)。
|
||||
*/
|
||||
@GetMapping("/work-method/{id}/rd-context")
|
||||
public ApiResp<RdTechContext> workMethodRdContext(@PathVariable Long id) {
|
||||
WorkMethod m = methodRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("工法不存在:" + id));
|
||||
String note = m.getRdProjectId() == null
|
||||
? "工法未关联研发立项,建议在工法详情中绑定 rdProjectId 以自动关联查新报告。"
|
||||
: "工法已关联研发立项「" + nz(m.getRdProjectName()) + "」(id=" + m.getRdProjectId()
|
||||
+ "),技术背景与工艺原理字段已回填,可作为专利交底书参考素材。";
|
||||
return ApiResp.ok(new RdTechContext(
|
||||
m.getId(), m.getMethodNo(), m.getName(),
|
||||
m.getRdProjectId(), m.getRdProjectName(),
|
||||
m.getTechBackground(), m.getContentPrinciple(),
|
||||
m.getInnovation(), m.getApplicableScope(),
|
||||
note));
|
||||
}
|
||||
|
||||
/**
|
||||
* 聚合视图:工法/专利的全部跨部门接口状态一览(IP 风险 + HR 发明人 + 归档状态 + RD 联动)。
|
||||
* 方便专利工法办一次性检查所有跨部门接口的完备性。
|
||||
*/
|
||||
@GetMapping("/work-method/{id}/cross-dept-summary")
|
||||
public ApiResp<Map<String, Object>> crossDeptSummary(@PathVariable Long id) {
|
||||
WorkMethod m = methodRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("工法不存在:" + id));
|
||||
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("workMethodId", m.getId());
|
||||
summary.put("methodNo", m.getMethodNo());
|
||||
summary.put("name", m.getName());
|
||||
summary.put("stage", m.getStage());
|
||||
|
||||
// IP 侵权风险
|
||||
String techField = nz(m.getTechField());
|
||||
List<CompetitorIp> competitors = techField.isBlank()
|
||||
? List.of() : competitorIpRepo.findByTechField(techField);
|
||||
summary.put("ipRiskCount", competitors.size());
|
||||
summary.put("ipRiskSummary", buildRiskSummary(competitors, techField, m.getName()));
|
||||
|
||||
// HR 完成人联动
|
||||
ContributorLookupResult contrib = lookupContributors(nz(m.getContributors()), "工法");
|
||||
summary.put("contributorCount", contrib.profiles().size());
|
||||
summary.put("contributorNote", contrib.note());
|
||||
summary.put("contributorProfiles", contrib.profiles());
|
||||
|
||||
// 归档申请状态
|
||||
String archiveTitle = "工法证书 [" + nz(m.getMethodNo()) + "] " + nz(m.getName());
|
||||
List<ArchiveSubmission> pending = archiveSubmissionRepo.findByStatus("待审核");
|
||||
List<ArchiveSubmission> archived = archiveSubmissionRepo.findByStatus("已归档");
|
||||
boolean hasPending = pending.stream().anyMatch(a -> archiveTitle.equals(a.getTitle()));
|
||||
boolean hasArchived = archived.stream().anyMatch(a -> archiveTitle.equals(a.getTitle()));
|
||||
summary.put("archiveStatus", hasArchived ? "已归档" : hasPending ? "归档申请待审核" : "未申请归档");
|
||||
summary.put("canTriggerArchive", "已批准".equals(m.getStage()) && !hasPending && !hasArchived);
|
||||
|
||||
// RD 联动
|
||||
summary.put("rdProjectId", m.getRdProjectId());
|
||||
summary.put("rdProjectName", m.getRdProjectName());
|
||||
summary.put("hasTechBackground", !nz(m.getTechBackground()).isBlank());
|
||||
summary.put("hasContentPrinciple", !nz(m.getContentPrinciple()).isBlank());
|
||||
|
||||
return ApiResp.ok(summary);
|
||||
}
|
||||
|
||||
// ---------- (5) 申报服务部:工法/专利证书专属推送(W7 Gap-10 补完) ----------
|
||||
|
||||
public record DeclPushRequest(Long declarationId, String pusher) {
|
||||
}
|
||||
|
||||
public record DeclPushResult(Long declarationId, String declarationName,
|
||||
String certRef, String pusher, String pushedAt, String message) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 工法证书推送到申报服务部指定申报记录。
|
||||
* 工法须「已批准」才有证书可推。将工法证书编号/批准文号追加到 Declaration.owner 字段(备注用途),
|
||||
* 让申报服务部填高企/专精特新/科技计划时可直接引用该工法证书记录,无需手工抄录。
|
||||
* 防重推送:同一工法证书编号已在 owner 中存在则 409。
|
||||
*/
|
||||
@PostMapping("/work-method/{id}/push-cert-to-declaration")
|
||||
@Transactional
|
||||
public ApiResp<DeclPushResult> pushWorkMethodCertToDeclaration(
|
||||
@PathVariable Long id, @RequestBody DeclPushRequest req) {
|
||||
WorkMethod m = methodRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("工法不存在:" + id));
|
||||
if (!"已批准".equals(m.getStage())) {
|
||||
throw new ApiException(409, "仅「已批准」工法有证书可推送,当前:" + m.getStage());
|
||||
}
|
||||
if (req.declarationId() == null) {
|
||||
throw new ApiException(400, "declarationId 不能为空");
|
||||
}
|
||||
Declaration decl = declarationRepo.findById(req.declarationId())
|
||||
.orElseThrow(() -> new NotFoundException("申报记录不存在:" + req.declarationId()));
|
||||
|
||||
String certNo = nz(m.getCertNo()).isBlank() ? (m.getMethodNo() + "-CERT") : m.getCertNo();
|
||||
String certRef = "[工法证书] " + nz(m.getMethodNo()) + " / " + nz(m.getName())
|
||||
+ " 证书编号:" + certNo + " 批准文号:" + nz(m.getApproveDocNo())
|
||||
+ " 有效期至:" + nz(m.getCertExpireDate());
|
||||
|
||||
String existingOwner = nz(decl.getOwner());
|
||||
if (existingOwner.contains(certNo)) {
|
||||
throw new ApiException(409, "工法证书「" + certNo + "」已推送到该申报记录,请勿重复推送。");
|
||||
}
|
||||
String newOwner = existingOwner.isBlank() ? certRef : existingOwner + "\n" + certRef;
|
||||
decl.setOwner(newOwner);
|
||||
declarationRepo.save(decl);
|
||||
|
||||
String now = LocalDate.now().toString();
|
||||
return ApiResp.ok(new DeclPushResult(decl.getId(), nz(decl.getName()), certRef,
|
||||
nz(req.pusher()), now, "工法证书已成功推送至申报记录「" + nz(decl.getName()) + "」,申报服务部可直接引用。"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 专利证书推送到申报服务部指定申报记录。
|
||||
* 专利须「已授权」才有证书可推。类似工法证书推送,将专利号/授权日追加到 Declaration.owner 备注,
|
||||
* 支持申报服务部填高企/专精特新知识产权清单时直接引用。
|
||||
*/
|
||||
@PostMapping("/patent/{id}/push-cert-to-declaration")
|
||||
@Transactional
|
||||
public ApiResp<DeclPushResult> pushPatentCertToDeclaration(
|
||||
@PathVariable Long id, @RequestBody DeclPushRequest req) {
|
||||
Patent p = patentRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("专利不存在:" + id));
|
||||
if (!"已授权".equals(p.getStatus())) {
|
||||
throw new ApiException(409, "仅「已授权」专利有证书可推送,当前:" + p.getStatus());
|
||||
}
|
||||
if (req.declarationId() == null) {
|
||||
throw new ApiException(400, "declarationId 不能为空");
|
||||
}
|
||||
Declaration decl = declarationRepo.findById(req.declarationId())
|
||||
.orElseThrow(() -> new NotFoundException("申报记录不存在:" + req.declarationId()));
|
||||
|
||||
String patentNo = nz(p.getPatentNo()).isBlank() ? "(专利号待录入)" : p.getPatentNo();
|
||||
String certRef = "[专利证书] " + nz(p.getType()) + " / " + nz(p.getName())
|
||||
+ " 专利号:" + patentNo + " 授权日:" + nz(p.getGrantDate())
|
||||
+ " IPC:" + nz(p.getIpcClass()) + " 技术领域:" + nz(p.getTechDomain());
|
||||
|
||||
String existingOwner = nz(decl.getOwner());
|
||||
if (existingOwner.contains(patentNo) && !patentNo.contains("待录入")) {
|
||||
throw new ApiException(409, "专利「" + patentNo + "」已推送到该申报记录,请勿重复推送。");
|
||||
}
|
||||
String newOwner = existingOwner.isBlank() ? certRef : existingOwner + "\n" + certRef;
|
||||
decl.setOwner(newOwner);
|
||||
declarationRepo.save(decl);
|
||||
|
||||
String now = LocalDate.now().toString();
|
||||
return ApiResp.ok(new DeclPushResult(decl.getId(), nz(decl.getName()), certRef,
|
||||
nz(req.pusher()), now, "专利证书已成功推送至申报记录「" + nz(decl.getName()) + "」,申报服务部可直接引用。"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览可推送到申报服务部的已批准工法证书和已授权专利证书清单。
|
||||
* 直接对标需求功能10「工法证书/专利证书 → 申报服务部」接口。
|
||||
*/
|
||||
@GetMapping("/cert-push-preview")
|
||||
public ApiResp<Map<String, Object>> certPushPreview() {
|
||||
List<WorkMethod> approvedMethods = methodRepo.findByStage("已批准");
|
||||
List<Patent> grantedPatents = patentRepo.findByStatus("已授权");
|
||||
|
||||
List<Map<String, Object>> methodRows = new ArrayList<>();
|
||||
for (WorkMethod m : approvedMethods) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("sourceType", "工法证书");
|
||||
row.put("id", m.getId());
|
||||
row.put("no", m.getMethodNo());
|
||||
row.put("name", m.getName());
|
||||
row.put("certNo", nz(m.getCertNo()));
|
||||
row.put("approveDocNo", nz(m.getApproveDocNo()));
|
||||
row.put("certExpireDate", nz(m.getCertExpireDate()));
|
||||
row.put("level", nz(m.getLevel()));
|
||||
row.put("techField", nz(m.getTechField()));
|
||||
methodRows.add(row);
|
||||
}
|
||||
|
||||
List<Map<String, Object>> patentRows = new ArrayList<>();
|
||||
for (Patent p : grantedPatents) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("sourceType", "专利证书");
|
||||
row.put("id", p.getId());
|
||||
row.put("patentNo", nz(p.getPatentNo()));
|
||||
row.put("name", p.getName());
|
||||
row.put("type", nz(p.getType()));
|
||||
row.put("grantDate", nz(p.getGrantDate()));
|
||||
row.put("ipcClass", nz(p.getIpcClass()));
|
||||
row.put("techDomain", nz(p.getTechDomain()));
|
||||
patentRows.add(row);
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("approvedWorkMethods", methodRows);
|
||||
result.put("grantedPatents", patentRows);
|
||||
result.put("totalWorkMethods", methodRows.size());
|
||||
result.put("totalPatents", patentRows.size());
|
||||
result.put("declarations", declarationRepo.findAll().stream().map(d -> {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("id", d.getId());
|
||||
m.put("name", nz(d.getName()));
|
||||
m.put("program", nz(d.getProgram()));
|
||||
m.put("status", nz(d.getStatus()));
|
||||
return m;
|
||||
}).toList());
|
||||
return ApiResp.ok(result);
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
private static String nz(String s) {
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user