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,232 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user