本地 RAG 知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

668 lines
25 KiB

package com.wok.supportbot.controller;
import com.wok.supportbot.entity.CategoryNode;
import com.wok.supportbot.entity.KnowledgeCategory;
import com.wok.supportbot.entity.KnowledgeDocument;
import com.wok.supportbot.entity.SearchResult;
import com.wok.supportbot.service.DocumentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* 知识库文档管理控制器
* 提供文档上传、查询、管理、分类、搜索等完整功能
*/
@RestController
public class DocumentController {
@Autowired
private DocumentService documentService;
// ==================== 上传校验常量 ====================
/** 允许上传的文件类型白名单 */
private static final Set<String> ALLOWED_EXTENSIONS = Set.of(
"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx",
"txt", "md", "json", "csv", "html", "xml", "rtf"
);
/** 文件大小上限(50MB,与 application.yml 中 multipart 配置一致) */
private static final long MAX_FILE_SIZE = 50 * 1024 * 1024;
/**
* 校验上传文件
* @param file 上传的文件
*/
private void validateUploadFile(MultipartFile file) {
if (file.getSize() > MAX_FILE_SIZE) {
throw new IllegalArgumentException("文件大小超过限制(最大 50MB)");
}
String extension = getFileExtension(file.getOriginalFilename());
if (extension != null && !ALLOWED_EXTENSIONS.contains(extension.toLowerCase())) {
throw new IllegalArgumentException("不支持的文件类型: " + extension);
}
}
/**
* 获取文件扩展名
*/
private String getFileExtension(String filename) {
if (filename == null || !filename.contains(".")) {
return null;
}
return filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
}
// ==================== 文档上传 ====================
/**
* 上传普通文件(支持多种格式),用 Tika 解析
*
* @param file 文件
* @param title 文档标题(可选,默认使用文件名)
* @param categoryId 分类ID(可选)
* @param tags 标签(可选)
* @return 上传结果
*/
@PostMapping("/upload/file")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> uploadFile(
@RequestParam("file") MultipartFile file,
@RequestParam(required = false) String title,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) List<String> tags) {
try {
validateUploadFile(file);
KnowledgeDocument doc = documentService.uploadFile(file, title, categoryId, tags);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "文件上传并向量化成功",
"data", doc
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "上传失败:" + e.getMessage()
));
}
}
/**
* 上传字符串内容
*
* @param content 内容
* @param title 标题
* @param categoryId 分类ID(可选)
* @param tags 标签(可选)
* @return 上传结果
*/
@PostMapping("/upload/string")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> uploadString(
@RequestBody String content,
@RequestParam String title,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) List<String> tags) {
try {
KnowledgeDocument doc = documentService.uploadString(content, title, categoryId, tags);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "文本内容上传并向量化成功",
"data", doc
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "上传失败:" + e.getMessage()
));
}
}
/**
* 上传 Markdown 文件
*/
@PostMapping("/upload/markdown")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> uploadMarkdown(
@RequestParam("file") MultipartFile file,
@RequestParam(required = false) String title,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) List<String> tags) {
try {
validateUploadFile(file);
KnowledgeDocument doc = documentService.uploadMarkdown(file, title, categoryId, tags);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "Markdown文件上传并向量化成功",
"data", doc
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "上传失败:" + e.getMessage()
));
}
}
/**
* 上传 JSON 文件(基本方式)
*/
@PostMapping("/upload/json/basic")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> uploadJsonBasic(
@RequestParam("file") MultipartFile file,
@RequestParam(required = false) String title,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) List<String> tags) {
try {
validateUploadFile(file);
KnowledgeDocument doc = documentService.uploadJsonBasic(file, title, categoryId, tags);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "JSON文件(基本方式)上传并向量化成功",
"data", doc
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "上传失败:" + e.getMessage()
));
}
}
/**
* 上传 JSON 文件(按字段提取)
*/
@PostMapping("/upload/json/fields")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> uploadJsonWithFields(
@RequestParam("file") MultipartFile file,
@RequestParam("fields") List<String> fields,
@RequestParam(required = false) String title,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) List<String> tags) {
try {
validateUploadFile(file);
KnowledgeDocument doc = documentService.uploadJsonFields(file, fields, title, categoryId, tags);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "JSON文件(按字段)上传并向量化成功",
"data", doc,
"extractedFields", fields
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "上传失败:" + e.getMessage()
));
}
}
/**
* 上传 JSON 文件(按指针拆分)
*/
@PostMapping("/upload/json/pointer")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> uploadJsonWithPointer(
@RequestParam("file") MultipartFile file,
@RequestParam("pointer") String pointer,
@RequestParam(required = false) String title,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) List<String> tags) {
try {
validateUploadFile(file);
KnowledgeDocument doc = documentService.uploadJsonPointer(file, pointer, title, categoryId, tags);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "JSON文件(按指针)上传并向量化成功",
"data", doc,
"pointer", pointer
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "上传失败:" + e.getMessage()
));
}
}
// ==================== 文档管理 ====================
/**
* 查询文档列表(分页 + 过滤)
*
* @param page 页码(默认1)
* @param size 每页大小(默认10)
* @param categoryId 分类ID过滤(可选)
* @param status 状态过滤(PROCESSING/READY/FAILED,可选)
* @return 分页文档列表
*/
@GetMapping("/document/list")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> listDocuments(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) String status) {
try {
Map<String, Object> result = documentService.listDocuments(page, size, categoryId, status);
Map<String, Object> data = new HashMap<>();
data.put("success", true);
data.put("data", result.get("records"));
data.put("total", result.get("total"));
data.put("page", result.get("page"));
data.put("size", result.get("size"));
data.put("pages", result.get("pages"));
return ResponseEntity.ok(data);
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "查询失败:" + e.getMessage()
));
}
}
/**
* 获取文档详情
*/
@GetMapping("/document/{id}")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> getDocumentDetail(@PathVariable Long id) {
try {
KnowledgeDocument doc = documentService.getDocumentDetail(id);
if (doc == null) {
return ResponseEntity.status(404).body(Map.of(
"success", false,
"message", "文档不存在"
));
}
return ResponseEntity.ok(Map.of(
"success", true,
"data", doc
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "查询失败:" + e.getMessage()
));
}
}
/**
* 获取文档的所有分块
*/
@GetMapping("/document/{id}/chunks")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> getDocumentChunks(@PathVariable Long id) {
try {
List<Map<String, Object>> chunks = documentService.getDocumentChunks(id);
return ResponseEntity.ok(Map.of(
"success", true,
"data", chunks,
"total", chunks.size()
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "查询失败:" + e.getMessage()
));
}
}
/**
* 删除文档(逻辑删除 + 级联删除向量)
*/
@DeleteMapping("/document/{id}")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> deleteDocument(@PathVariable Long id) {
try {
int vectorCount = documentService.deleteDocument(id);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "删除成功",
"deletedVectors", vectorCount
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "删除失败:" + e.getMessage()
));
}
}
/**
* 批量删除文档
*/
@PostMapping("/document/batch/delete")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> batchDeleteDocuments(@RequestBody Map<String, Object> body) {
try {
List<Long> ids = extractIds(body);
if (ids.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", "请提供要删除的文档ID列表"
));
}
Map<String, Object> result = documentService.batchDeleteDocuments(ids);
return ResponseEntity.ok(Map.of(
"success", true,
"message", String.format("批量删除完成:成功 %d 个,失败 %d 个",
result.get("successCount"), result.get("failCount")),
"data", result
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "批量删除失败:" + e.getMessage()
));
}
}
/**
* 批量重新处理文档
*/
@PostMapping("/document/batch/reprocess")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> batchReprocessDocuments(@RequestBody Map<String, Object> body) {
try {
List<Long> ids = extractIds(body);
if (ids.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", "请提供要重新处理的文档ID列表"
));
}
Map<String, Object> result = documentService.batchReprocessDocuments(ids);
return ResponseEntity.ok(Map.of(
"success", true,
"message", String.format("批量重新处理完成:成功 %d 个,失败 %d 个",
result.get("successCount"), result.get("failCount")),
"data", result
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "批量重新处理失败:" + e.getMessage()
));
}
}
/**
* 从请求体中提取 ID 列表(兼容字符串和数字类型的 ID)
*/
@SuppressWarnings("unchecked")
private List<Long> extractIds(Map<String, Object> body) {
List<?> rawIds = (List<?>) body.get("ids");
if (rawIds == null || rawIds.isEmpty()) {
return List.of();
}
return rawIds.stream().map(id -> {
if (id instanceof Number) {
return ((Number) id).longValue();
} else if (id instanceof String) {
return Long.parseLong((String) id);
} else {
throw new IllegalArgumentException("无效的ID格式: " + id);
}
}).toList();
}
/**
* 更新文档元信息
*/
@PutMapping("/document/{id}")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> updateDocument(
@PathVariable Long id,
@RequestParam(required = false) String title,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) List<String> tags) {
try {
documentService.updateDocumentMetadata(id, title, categoryId, tags);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "更新成功"
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "更新失败:" + e.getMessage()
));
}
}
/**
* 重新处理文档(重新分块 + 向量化)
*/
@PutMapping("/document/{id}/reprocess")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> reprocessDocument(@PathVariable Long id) {
try {
KnowledgeDocument doc = documentService.reprocessDocument(id);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "重新处理成功",
"data", doc
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "重新处理失败:" + e.getMessage()
));
}
}
// ==================== 语义搜索 ====================
/**
* 语义搜索
*
* @param body 搜索参数
* @return 搜索结果
*/
@PostMapping("/document/search")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> searchDocuments(@RequestBody Map<String, Object> body) {
try {
String query = (String) body.get("query");
int topK = body.get("topK") != null ? ((Number) body.get("topK")).intValue() : 5;
double similarityThreshold = body.get("similarityThreshold") != null
? ((Number) body.get("similarityThreshold")).doubleValue() : 0.5;
Long categoryId = body.get("categoryId") != null ? ((Number) body.get("categoryId")).longValue() : null;
List<Long> categoryIds = parseCategoryIds(body.get("categoryIds"));
if (categoryIds.isEmpty() && categoryId != null) {
categoryIds = List.of(categoryId);
}
String searchMode = (String) body.get("searchMode");
List<SearchResult> results;
if (searchMode != null && !searchMode.isEmpty()) {
results = documentService.searchDocuments(query, topK, similarityThreshold, categoryIds, searchMode);
} else {
results = documentService.searchDocuments(query, topK, similarityThreshold, categoryIds);
}
return ResponseEntity.ok(Map.of(
"success", true,
"data", results,
"total", results.size()
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "搜索失败:" + e.getMessage()
));
}
}
private List<Long> parseCategoryIds(Object rawCategoryIds) {
if (rawCategoryIds == null) {
return List.of();
}
if (rawCategoryIds instanceof List<?> list) {
return list.stream()
.filter(Objects::nonNull)
.map(item -> item instanceof Number number ? number.longValue() : Long.parseLong(item.toString()))
.filter(id -> id > 0)
.distinct()
.toList();
}
String text = rawCategoryIds.toString();
if (text.isBlank()) {
return List.of();
}
return Arrays.stream(text.split(","))
.map(String::trim)
.filter(item -> !item.isEmpty())
.map(Long::parseLong)
.filter(id -> id > 0)
.distinct()
.toList();
}
// ==================== 统计 ====================
/**
* 知识库统计面板
*/
@GetMapping("/document/stats")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> getStats() {
try {
Map<String, Object> stats = documentService.getStats();
return ResponseEntity.ok(Map.of(
"success", true,
"data", stats
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "查询统计失败:" + e.getMessage()
));
}
}
// ==================== 分类管理 ====================
/**
* 获取分类树
*/
@GetMapping("/category/tree")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> getCategoryTree() {
try {
List<CategoryNode> tree = documentService.getCategoryTree();
return ResponseEntity.ok(Map.of(
"success", true,
"data", tree
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "获取分类树失败:" + e.getMessage()
));
}
}
/**
* 获取分类列表
*/
@GetMapping("/category/list")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> listCategories() {
try {
List<KnowledgeCategory> list = documentService.listCategories();
return ResponseEntity.ok(Map.of(
"success", true,
"data", list
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "获取分类列表失败:" + e.getMessage()
));
}
}
/**
* 创建分类
*/
@PostMapping("/category")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> createCategory(@RequestBody Map<String, Object> body) {
try {
String name = (String) body.get("name");
String description = (String) body.get("description");
Long parentId = body.get("parentId") != null ? ((Number) body.get("parentId")).longValue() : null;
Integer sortOrder = body.get("sortOrder") != null ? ((Number) body.get("sortOrder")).intValue() : null;
KnowledgeCategory category = documentService.createCategory(name, description, parentId, sortOrder);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "分类创建成功",
"data", category
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "创建分类失败:" + e.getMessage()
));
}
}
/**
* 更新分类
*/
@PutMapping("/category/{id}")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> updateCategory(
@PathVariable Long id,
@RequestBody Map<String, Object> body) {
try {
String name = (String) body.get("name");
String description = (String) body.get("description");
Integer sortOrder = body.get("sortOrder") != null ? ((Number) body.get("sortOrder")).intValue() : null;
documentService.updateCategory(id, name, description, sortOrder);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "分类更新成功"
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "更新分类失败:" + e.getMessage()
));
}
}
/**
* 删除分类
*/
@DeleteMapping("/category/{id}")
@PreAuthorize("hasAnyRole('admin','kb_operator')")
public ResponseEntity<Map<String, Object>> deleteCategory(@PathVariable Long id) {
try {
documentService.deleteCategory(id);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "分类删除成功"
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "删除分类失败:" + e.getMessage()
));
}
}
}