恢复点(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>
233 lines
11 KiB
Java
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();
|
|
}
|
|
}
|