Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/IpArchiveSearchController.java
T
QiufengandClaude Opus 4.8 5e51dc3f56 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>
2026-06-15 19:19:15 +08:00

233 lines
11 KiB
Java

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<SearchResult> hits) {
}
/**
* 跨类型全文检索:在 IpKnowledge / Archive / TechAchievement / TechContract 中
* 按关键词匹配标题/名称/关键词/正文/备注,返回聚合结果列表。
*
* @param keyword 检索关键词(必填,空字符串返回 400)
* @param types 可选,逗号分隔的类型过滤(ip_knowledge/archive/tech_achieve/tech_contract),
* 不传或传 all 则全类型检索
*/
@GetMapping
public ApiResp<SearchResponse> 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<String> typeList = new ArrayList<>();
if (!all) {
for (String t : types.split(",")) {
String tt = t.trim();
if (!tt.isEmpty()) typeList.add(tt);
}
}
List<SearchResult> 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<DirNode> children) {
}
public record DirTree(List<DirNode> roots) {
}
/**
* 归档目录树(二级结构):补完「归档目录无树形层级深度(仅一层ipType,无二级目录)」缺口。
* 一级节点=归档类型(ip_knowledge/archive/tech_achieve/tech_contract);
* 二级节点=该类型内的分类枚举值(docType/category/achType/contractType)及该目录下资源数量。
*/
@GetMapping("/dir-tree")
public ApiResp<DirTree> dirTree() {
List<DirNode> roots = new ArrayList<>();
// ip_knowledge 目录
java.util.Map<String, Integer> 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<DirNode> knowledgeChildren = new ArrayList<>();
for (java.util.Map.Entry<String, Integer> 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<String, Integer> 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<DirNode> archiveChildren = new ArrayList<>();
for (java.util.Map.Entry<String, Integer> 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<String, Integer> 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<DirNode> achieveChildren = new ArrayList<>();
for (java.util.Map.Entry<String, Integer> 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<String, Integer> 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<DirNode> contractChildren = new ArrayList<>();
for (java.util.Map.Entry<String, Integer> 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<String, Integer> map) {
return map.values().stream().mapToInt(Integer::intValue).sum();
}
}