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,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;
}
}