package com.kaidi.oa.web; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.domain.Archive; import com.kaidi.oa.domain.IpKnowledge; import com.kaidi.oa.domain.TechAchievement; import com.kaidi.oa.domain.TechContract; import com.kaidi.oa.repository.ArchiveRepository; import com.kaidi.oa.repository.IpKnowledgeRepository; import com.kaidi.oa.repository.TechAchievementRepository; import com.kaidi.oa.repository.TechContractRepository; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; /** * 知识产权/研发归档中心跨类型全文检索(知识产权部,需求功能13「资料归档中心」缺口补完)。 * * 补审计缺口:「无跨类型通用全文检索引擎(IpKnowledge 有关键词搜索但仅针对知识库条目, * 其他归档类型无统一全文检索)」: * - 统一搜索接口 GET /ip-archive-search?keyword=&types=... * 跨 IpKnowledge(知识库)+ Archive(通用档案)+ TechAchievement(科技成果)+ * TechContract(技术合同)四类归档资源,在标题/名称/关键词/正文/备注字段中匹配; * - 结果按类型分组返回(SearchResult 含 resourceType/resourceId/title/snippet/extra); * - types 参数可选,逗号分隔过滤类型(ip_knowledge/archive/tech_achieve/tech_contract/all); * - 目录树端点 GET /ip-archive-search/dir-tree:返回二级目录结构 * (一级=归档类型,二级=ipType/category/achType 等枚举值 + 该目录下的资源数量), * 补完「归档目录无树形层级深度(仅一层ipType,无二级目录)」缺口; * - 本控制器纯只读,无写操作,默认受 AuthInterceptor 保护。 */ @RestController @RequestMapping("/api/oa/ip-archive-search") public class IpArchiveSearchController { private final IpKnowledgeRepository knowledgeRepo; private final ArchiveRepository archiveRepo; private final TechAchievementRepository achieveRepo; private final TechContractRepository contractRepo; public IpArchiveSearchController(IpKnowledgeRepository knowledgeRepo, ArchiveRepository archiveRepo, TechAchievementRepository achieveRepo, TechContractRepository contractRepo) { this.knowledgeRepo = knowledgeRepo; this.archiveRepo = archiveRepo; this.achieveRepo = achieveRepo; this.contractRepo = contractRepo; } public record SearchResult(String resourceType, Long resourceId, String title, String snippet, String extra, String status) { } public record SearchResponse(String keyword, int total, List hits) { } /** * 跨类型全文检索:在 IpKnowledge / Archive / TechAchievement / TechContract 中 * 按关键词匹配标题/名称/关键词/正文/备注,返回聚合结果列表。 * * @param keyword 检索关键词(必填,空字符串返回 400) * @param types 可选,逗号分隔的类型过滤(ip_knowledge/archive/tech_achieve/tech_contract), * 不传或传 all 则全类型检索 */ @GetMapping public ApiResp search(@RequestParam String keyword, @RequestParam(required = false) String types) { String kw = keyword == null ? "" : keyword.trim(); if (kw.isEmpty()) { throw new ApiException(400, "检索关键词不能为空"); } boolean all = types == null || types.isBlank() || "all".equals(types.trim()); List typeList = new ArrayList<>(); if (!all) { for (String t : types.split(",")) { String tt = t.trim(); if (!tt.isEmpty()) typeList.add(tt); } } List hits = new ArrayList<>(); // --- IpKnowledge --- if (all || typeList.contains("ip_knowledge")) { for (IpKnowledge k : knowledgeRepo.findAll()) { if (contains(k.getTitle(), kw) || contains(k.getKeywords(), kw) || contains(k.getTechField(), kw) || contains(k.getContent(), kw)) { hits.add(new SearchResult("ip_knowledge", k.getId(), k.getTitle(), snippet(k.getContent(), kw), "类型:" + k.getDocType() + " 技术分类:" + k.getTechField(), null)); } } } // --- Archive --- if (all || typeList.contains("archive")) { for (Archive a : archiveRepo.findAll()) { if (contains(a.getTitle(), kw) || contains(a.getTags(), kw) || contains(a.getCategory(), kw)) { hits.add(new SearchResult("archive", a.getId(), a.getTitle(), "分类:" + a.getCategory() + " 标签:" + a.getTags(), "密级:" + a.getAccessLevel(), null)); } } } // --- TechAchievement --- if (all || typeList.contains("tech_achieve")) { for (TechAchievement a : achieveRepo.findAll()) { if (contains(a.getName(), kw) || contains(a.getAchType(), kw) || contains(a.getEvalLevel(), kw) || contains(a.getEvidence(), kw)) { hits.add(new SearchResult("tech_achieve", a.getId(), a.getName(), "成果类型:" + a.getAchType() + " 评价等级:" + a.getEvalLevel(), "登记号:" + a.getRegNo(), a.getStatus())); } } } // --- TechContract --- if (all || typeList.contains("tech_contract")) { for (TechContract c : contractRepo.findAll()) { if (contains(c.getProjectName(), kw) || contains(c.getBuyer(), kw) || contains(c.getSeller(), kw) || contains(c.getTechScheme(), kw)) { hits.add(new SearchResult("tech_contract", c.getId(), c.getProjectName(), "合同类型:" + c.getContractType() + " 买方:" + c.getBuyer(), "登记号:" + c.getRegisterNo(), c.getRegisterStatus())); } } } return ApiResp.ok(new SearchResponse(kw, hits.size(), hits)); } // ---------- 二级目录树(归档类型 → 二级分类枚举 + 资源数)---------- public record DirNode(String label, String type, int count, List children) { } public record DirTree(List roots) { } /** * 归档目录树(二级结构):补完「归档目录无树形层级深度(仅一层ipType,无二级目录)」缺口。 * 一级节点=归档类型(ip_knowledge/archive/tech_achieve/tech_contract); * 二级节点=该类型内的分类枚举值(docType/category/achType/contractType)及该目录下资源数量。 */ @GetMapping("/dir-tree") public ApiResp dirTree() { List roots = new ArrayList<>(); // ip_knowledge 目录 java.util.Map knowledgeByCat = new java.util.LinkedHashMap<>(); for (IpKnowledge k : knowledgeRepo.findAll()) { String cat = k.getDocType() == null || k.getDocType().isBlank() ? "其他" : k.getDocType(); knowledgeByCat.merge(cat, 1, Integer::sum); } List knowledgeChildren = new ArrayList<>(); for (java.util.Map.Entry e : knowledgeByCat.entrySet()) { knowledgeChildren.add(new DirNode(e.getKey(), "ip_knowledge_sub", e.getValue(), List.of())); } roots.add(new DirNode("知识产权知识库", "ip_knowledge", totalSize(knowledgeByCat), knowledgeChildren)); // archive 目录 java.util.Map archiveByCat = new java.util.LinkedHashMap<>(); for (Archive a : archiveRepo.findAll()) { String cat = a.getCategory() == null || a.getCategory().isBlank() ? "其他" : a.getCategory(); archiveByCat.merge(cat, 1, Integer::sum); } List archiveChildren = new ArrayList<>(); for (java.util.Map.Entry e : archiveByCat.entrySet()) { archiveChildren.add(new DirNode(e.getKey(), "archive_sub", e.getValue(), List.of())); } roots.add(new DirNode("通用档案", "archive", totalSize(archiveByCat), archiveChildren)); // tech_achieve 目录 java.util.Map achieveByCat = new java.util.LinkedHashMap<>(); for (TechAchievement a : achieveRepo.findAll()) { String cat = a.getAchType() == null || a.getAchType().isBlank() ? "其他" : a.getAchType(); achieveByCat.merge(cat, 1, Integer::sum); } List achieveChildren = new ArrayList<>(); for (java.util.Map.Entry e : achieveByCat.entrySet()) { achieveChildren.add(new DirNode(e.getKey(), "tech_achieve_sub", e.getValue(), List.of())); } roots.add(new DirNode("科技成果", "tech_achieve", totalSize(achieveByCat), achieveChildren)); // tech_contract 目录 java.util.Map contractByCat = new java.util.LinkedHashMap<>(); for (TechContract c : contractRepo.findAll()) { String cat = c.getContractType() == null || c.getContractType().isBlank() ? "其他" : c.getContractType(); contractByCat.merge(cat, 1, Integer::sum); } List contractChildren = new ArrayList<>(); for (java.util.Map.Entry e : contractByCat.entrySet()) { contractChildren.add(new DirNode(e.getKey(), "tech_contract_sub", e.getValue(), List.of())); } roots.add(new DirNode("技术合同", "tech_contract", totalSize(contractByCat), contractChildren)); return ApiResp.ok(new DirTree(roots)); } // ---------- helpers ---------- private boolean contains(String field, String kw) { return field != null && field.contains(kw); } private String snippet(String content, String kw) { if (content == null || kw == null) return ""; int idx = content.indexOf(kw); if (idx < 0) return ""; int start = Math.max(0, idx - 30); int end = Math.min(content.length(), idx + kw.length() + 60); return (start > 0 ? "..." : "") + content.substring(start, end) + (end < content.length() ? "..." : ""); } private int totalSize(java.util.Map map) { return map.values().stream().mapToInt(Integer::intValue).sum(); } }