feat(rag): 补齐知识工作台聚合与转换分层

This commit is contained in:
2026-06-01 03:38:41 +08:00
parent 07ad8bb36b
commit d9cf838ace
15 changed files with 823 additions and 79 deletions

View File

@@ -0,0 +1,36 @@
package com.bruce.rag.controller;
import com.bruce.common.domain.model.RequestResult;
import com.bruce.rag.service.IKnowledgeWorkspaceService;
import com.bruce.rag.vo.KnowledgeWorkspaceVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 知识工作台聚合接口。
*/
@Tag(name = "知识工作台")
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/knowledge/workspaces")
public class KnowledgeWorkspaceController {
private final IKnowledgeWorkspaceService knowledgeWorkspaceService;
@Operation(summary = "查询知识工作台聚合视图")
@GetMapping("/{storeId}")
public RequestResult<KnowledgeWorkspaceVO> getWorkspace(@PathVariable("storeId") Long storeId) {
log.info("KnowledgeWorkspaceController.getWorkspace start, storeId={}", storeId);
KnowledgeWorkspaceVO workspace = knowledgeWorkspaceService.getWorkspace(storeId);
log.info("KnowledgeWorkspaceController.getWorkspace success, storeId={}, documentCount={}, healthScore={}",
storeId, workspace.getDocumentCount(), workspace.getHealthScore());
return RequestResult.success(workspace);
}
}

View File

@@ -0,0 +1,71 @@
package com.bruce.rag.factory;
import com.bruce.rag.dto.request.RagDocumentSaveRequest;
import com.bruce.rag.dto.response.RagDocumentResponse;
import com.bruce.rag.entity.RagDocument;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* 知识文档工厂,统一处理文档相关 DTO、Entity、VO 的转换逻辑。
*/
@Component
public class RagDocumentFactory {
/**
* 将保存请求转换为实体,供新增或全量更新场景复用。
*/
public RagDocument toEntity(RagDocumentSaveRequest request) {
if (request == null) {
return null;
}
RagDocument entity = new RagDocument();
applyEditableFields(entity, request);
return entity;
}
/**
* 将请求中的可编辑字段写入已有实体,避免服务层散落 setXXX 逻辑。
*/
public void applyEditableFields(RagDocument entity, RagDocumentSaveRequest request) {
if (entity == null || request == null) {
return;
}
entity.setId(request.getId());
entity.setStoreId(request.getStoreId());
entity.setAttachmentId(request.getAttachmentId());
entity.setDocumentTitle(trimToNull(request.getDocumentTitle()));
entity.setDocumentSummary(trimToNull(request.getDocumentSummary()));
entity.setParseStatus(trimToNull(request.getParseStatus()));
entity.setIndexStatus(trimToNull(request.getIndexStatus()));
entity.setEnabled(request.getEnabled());
entity.setErrorMessage(trimToNull(request.getErrorMessage()));
entity.setRemark(trimToNull(request.getRemark()));
}
/**
* 将实体转换为前端使用的返回对象。
*/
public RagDocumentResponse toResponse(RagDocument entity) {
if (entity == null) {
return null;
}
RagDocumentResponse response = new RagDocumentResponse();
BeanUtils.copyProperties(entity, response);
return response;
}
public List<RagDocumentResponse> toResponses(List<RagDocument> entities) {
return entities == null ? List.of() : entities.stream().map(this::toResponse).toList();
}
private String trimToNull(String value) {
if (!StringUtils.hasText(value)) {
return null;
}
return value.trim();
}
}

View File

@@ -0,0 +1,57 @@
package com.bruce.rag.factory;
import com.bruce.rag.dto.request.RagStoreSaveRequest;
import com.bruce.rag.dto.response.RagStoreResponse;
import com.bruce.rag.entity.RagStore;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* 知识库工厂,统一负责知识库请求对象、实体对象和返回对象之间的转换。
*/
@Component
public class RagStoreFactory {
/**
* 将保存请求转换为知识库实体,统一处理空白裁剪规则。
*/
public RagStore toEntity(RagStoreSaveRequest request) {
if (request == null) {
return null;
}
RagStore entity = new RagStore();
entity.setId(request.getId());
entity.setStoreCode(trimToNull(request.getStoreCode()));
entity.setStoreName(trimToNull(request.getStoreName()));
entity.setDescription(trimToNull(request.getDescription()));
entity.setStatus(trimToNull(request.getStatus()));
entity.setRemark(trimToNull(request.getRemark()));
return entity;
}
/**
* 将实体转换为返回对象,避免接口层直接暴露实体结构。
*/
public RagStoreResponse toResponse(RagStore entity) {
if (entity == null) {
return null;
}
RagStoreResponse response = new RagStoreResponse();
BeanUtils.copyProperties(entity, response);
return response;
}
public List<RagStoreResponse> toResponses(List<RagStore> entities) {
return entities == null ? List.of() : entities.stream().map(this::toResponse).toList();
}
private String trimToNull(String value) {
if (!StringUtils.hasText(value)) {
return null;
}
return value.trim();
}
}

View File

@@ -0,0 +1,14 @@
package com.bruce.rag.service;
import com.bruce.rag.vo.KnowledgeWorkspaceVO;
/**
* 知识工作台聚合服务,对外提供知识资产工作台需要的统一视图。
*/
public interface IKnowledgeWorkspaceService {
/**
* 按知识库ID聚合知识工作台视图。
*/
KnowledgeWorkspaceVO getWorkspace(Long storeId);
}

View File

@@ -0,0 +1,139 @@
package com.bruce.rag.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.bruce.modelprovider.dto.response.RagStoreModelConfigResponse;
import com.bruce.modelprovider.service.IRagStoreModelConfigService;
import com.bruce.rag.dto.request.RagDocumentQueryRequest;
import com.bruce.rag.dto.response.RagDocumentResponse;
import com.bruce.rag.dto.response.RagStoreResponse;
import com.bruce.rag.entity.RagChunk;
import com.bruce.rag.entity.RagChunkEmbedding;
import com.bruce.rag.service.IKnowledgeWorkspaceService;
import com.bruce.rag.service.IRagChunkEmbeddingService;
import com.bruce.rag.service.IRagChunkService;
import com.bruce.rag.service.IRagDocumentService;
import com.bruce.rag.service.IRagStoreService;
import com.bruce.rag.vo.KnowledgeWorkspaceDocumentVO;
import com.bruce.rag.vo.KnowledgeWorkspaceVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 知识工作台聚合服务实现。
* <p>
* 该服务负责把知识库基础信息、文档状态、模型绑定和索引结果拼成前端工作台可直接消费的聚合视图。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class KnowledgeWorkspaceServiceImpl implements IKnowledgeWorkspaceService {
private static final String PARSE_STATUS_PARSED = "PARSED";
private static final String PARSE_STATUS_FAILED = "FAILED";
private static final String INDEX_STATUS_INDEXED = "INDEXED";
private static final String INDEX_STATUS_PENDING = "PENDING";
private final IRagStoreService ragStoreService;
private final IRagDocumentService ragDocumentService;
private final IRagChunkService ragChunkService;
private final IRagChunkEmbeddingService ragChunkEmbeddingService;
private final IRagStoreModelConfigService ragStoreModelConfigService;
@Override
public KnowledgeWorkspaceVO getWorkspace(Long storeId) {
log.info("知识工作台查询开始storeId={}", storeId);
if (storeId == null) {
throw new IllegalArgumentException("知识库ID不能为空");
}
RagStoreResponse store = ragStoreService.getResponseById(storeId);
if (store == null) {
throw new IllegalArgumentException("知识库不存在ID: " + storeId);
}
RagDocumentQueryRequest queryRequest = new RagDocumentQueryRequest();
queryRequest.setStoreId(storeId);
List<RagDocumentResponse> documents = ragDocumentService.query(queryRequest);
RagStoreModelConfigResponse modelConfig = ragStoreModelConfigService.getByStoreId(storeId);
long chunkCount = ragChunkService.count(new LambdaQueryWrapper<RagChunk>()
.eq(RagChunk::getStoreId, storeId)
.eq(RagChunk::getEnabled, true));
long embeddingCount = ragChunkEmbeddingService.count(new LambdaQueryWrapper<RagChunkEmbedding>()
.eq(RagChunkEmbedding::getStoreId, storeId)
.eq(RagChunkEmbedding::getEnabled, true));
KnowledgeWorkspaceVO workspace = new KnowledgeWorkspaceVO();
workspace.setStoreId(store.getId());
workspace.setStoreCode(store.getStoreCode());
workspace.setStoreName(store.getStoreName());
workspace.setDescription(store.getDescription());
workspace.setStatus(store.getStatus());
workspace.setRemark(store.getRemark());
workspace.setDocumentCount(documents.size());
workspace.setParsedDocumentCount((int) documents.stream().filter(this::isParsed).count());
workspace.setParseFailedDocumentCount((int) documents.stream().filter(this::isParseFailed).count());
workspace.setIndexedDocumentCount((int) documents.stream().filter(this::isIndexed).count());
workspace.setPendingIndexDocumentCount((int) documents.stream().filter(this::isPendingIndex).count());
workspace.setHealthScore(calculateHealthScore(documents.size(), workspace.getParseFailedDocumentCount()));
workspace.setChunkCount(chunkCount);
workspace.setEmbeddingCount(embeddingCount);
workspace.setPendingTaskCount(workspace.getPendingIndexDocumentCount());
workspace.setPublishImpact("更新后需要重新验证 Workflow / Agent 的知识引用效果");
workspace.setDocuments(documents.stream().map(this::toWorkspaceDocument).toList());
if (modelConfig != null) {
workspace.setEmbeddingModelId(modelConfig.getEmbeddingModelId());
workspace.setEmbeddingDimension(modelConfig.getEmbeddingDimension());
workspace.setChunkStrategy(modelConfig.getChunkStrategy());
workspace.setChunkSize(modelConfig.getChunkSize());
workspace.setChunkOverlap(modelConfig.getChunkOverlap());
workspace.setIndexVersion(modelConfig.getIndexVersion());
}
log.info("知识工作台查询结束storeId={}, documentCount={}, healthScore={}, pendingTaskCount={}",
storeId, workspace.getDocumentCount(), workspace.getHealthScore(), workspace.getPendingTaskCount());
return workspace;
}
private KnowledgeWorkspaceDocumentVO toWorkspaceDocument(RagDocumentResponse document) {
KnowledgeWorkspaceDocumentVO vo = new KnowledgeWorkspaceDocumentVO();
vo.setDocumentId(document.getId());
vo.setDocumentTitle(document.getDocumentTitle());
vo.setParseStatus(document.getParseStatus());
vo.setIndexStatus(document.getIndexStatus());
vo.setEnabled(document.getEnabled());
vo.setUpdateTime(document.getUpdateTime());
return vo;
}
private boolean isParsed(RagDocumentResponse document) {
return PARSE_STATUS_PARSED.equals(document.getParseStatus());
}
private boolean isParseFailed(RagDocumentResponse document) {
return PARSE_STATUS_FAILED.equals(document.getParseStatus());
}
private boolean isIndexed(RagDocumentResponse document) {
return INDEX_STATUS_INDEXED.equals(document.getIndexStatus());
}
private boolean isPendingIndex(RagDocumentResponse document) {
return INDEX_STATUS_PENDING.equals(document.getIndexStatus());
}
/**
* 健康度按“非失败文档占比”计算,便于前端快速识别当前知识库是否适合继续发布引用。
*/
private int calculateHealthScore(int totalDocumentCount, int parseFailedDocumentCount) {
if (totalDocumentCount <= 0) {
return 100;
}
int healthyDocumentCount = Math.max(totalDocumentCount - parseFailedDocumentCount, 0);
return (int) Math.floor(healthyDocumentCount * 100.0 / totalDocumentCount);
}
}

View File

@@ -12,11 +12,12 @@ import com.bruce.rag.dto.response.RagDocumentResponse;
import com.bruce.rag.entity.RagDocument; import com.bruce.rag.entity.RagDocument;
import com.bruce.rag.enums.RagIndexStatusEnum; import com.bruce.rag.enums.RagIndexStatusEnum;
import com.bruce.rag.enums.RagParseStatusEnum; import com.bruce.rag.enums.RagParseStatusEnum;
import com.bruce.rag.factory.RagDocumentFactory;
import com.bruce.rag.mapper.RagDocumentMapper; import com.bruce.rag.mapper.RagDocumentMapper;
import com.bruce.rag.service.IRagDocumentAutoParseService; import com.bruce.rag.service.IRagDocumentAutoParseService;
import com.bruce.rag.service.IRagDocumentService; import com.bruce.rag.service.IRagDocumentService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@@ -25,18 +26,17 @@ import java.util.List;
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor
public class RagDocumentServiceImpl extends ServiceImpl<RagDocumentMapper, RagDocument> implements IRagDocumentService { public class RagDocumentServiceImpl extends ServiceImpl<RagDocumentMapper, RagDocument> implements IRagDocumentService {
@Autowired private final ISysAttachmentService sysAttachmentService;
private ISysAttachmentService sysAttachmentService; private final IRagDocumentAutoParseService ragDocumentAutoParseService;
private final RagDocumentFactory ragDocumentFactory;
@Autowired
private IRagDocumentAutoParseService ragDocumentAutoParseService;
@Override @Override
public List<RagDocumentResponse> listResponses() { public List<RagDocumentResponse> listResponses() {
log.info("RagDocumentServiceImpl.listResponses start"); log.info("RagDocumentServiceImpl.listResponses start");
List<RagDocumentResponse> responses = toResponses(list()); List<RagDocumentResponse> responses = ragDocumentFactory.toResponses(list());
log.info("RagDocumentServiceImpl.listResponses success, count={}", responses.size()); log.info("RagDocumentServiceImpl.listResponses success, count={}", responses.size());
return responses; return responses;
} }
@@ -47,7 +47,7 @@ public class RagDocumentServiceImpl extends ServiceImpl<RagDocumentMapper, RagDo
RagDocumentQueryRequest queryRequest = request == null ? new RagDocumentQueryRequest() : request; RagDocumentQueryRequest queryRequest = request == null ? new RagDocumentQueryRequest() : request;
String parseStatus = trimToNull(queryRequest.getParseStatus()); String parseStatus = trimToNull(queryRequest.getParseStatus());
String indexStatus = trimToNull(queryRequest.getIndexStatus()); String indexStatus = trimToNull(queryRequest.getIndexStatus());
List<RagDocumentResponse> responses = toResponses(lambdaQuery() List<RagDocumentResponse> responses = ragDocumentFactory.toResponses(lambdaQuery()
.eq(queryRequest.getStoreId() != null, RagDocument::getStoreId, queryRequest.getStoreId()) .eq(queryRequest.getStoreId() != null, RagDocument::getStoreId, queryRequest.getStoreId())
.eq(queryRequest.getAttachmentId() != null, RagDocument::getAttachmentId, queryRequest.getAttachmentId()) .eq(queryRequest.getAttachmentId() != null, RagDocument::getAttachmentId, queryRequest.getAttachmentId())
.eq(parseStatus != null, RagDocument::getParseStatus, parseStatus) .eq(parseStatus != null, RagDocument::getParseStatus, parseStatus)
@@ -62,7 +62,7 @@ public class RagDocumentServiceImpl extends ServiceImpl<RagDocumentMapper, RagDo
@Override @Override
public RagDocumentResponse getResponseById(Long id) { public RagDocumentResponse getResponseById(Long id) {
log.info("RagDocumentServiceImpl.getResponseById start, id={}", id); log.info("RagDocumentServiceImpl.getResponseById start, id={}", id);
RagDocumentResponse response = RagDocumentResponse.fromEntity(getById(id)); RagDocumentResponse response = ragDocumentFactory.toResponse(getById(id));
log.info("RagDocumentServiceImpl.getResponseById success, id={}, found={}", id, response != null); log.info("RagDocumentServiceImpl.getResponseById success, id={}, found={}", id, response != null);
return response; return response;
} }
@@ -85,34 +85,7 @@ public class RagDocumentServiceImpl extends ServiceImpl<RagDocumentMapper, RagDo
document.setParseStatus(RagParseStatusEnum.UPLOADED.name()); document.setParseStatus(RagParseStatusEnum.UPLOADED.name());
document.setIndexStatus(RagIndexStatusEnum.PENDING.name()); document.setIndexStatus(RagIndexStatusEnum.PENDING.name());
} }
applyRequestToDocument(document, request);
if (request.getStoreId() != null) {
document.setStoreId(request.getStoreId());
}
if (request.getAttachmentId() != null) {
document.setAttachmentId(request.getAttachmentId());
}
if (StringUtils.hasText(request.getDocumentTitle())) {
document.setDocumentTitle(request.getDocumentTitle().trim());
}
if (request.getDocumentSummary() != null) {
document.setDocumentSummary(trimToNull(request.getDocumentSummary()));
}
if (StringUtils.hasText(request.getParseStatus())) {
document.setParseStatus(request.getParseStatus().trim());
}
if (StringUtils.hasText(request.getIndexStatus())) {
document.setIndexStatus(request.getIndexStatus().trim());
}
if (request.getEnabled() != null) {
document.setEnabled(request.getEnabled());
}
if (request.getErrorMessage() != null) {
document.setErrorMessage(trimToNull(request.getErrorMessage()));
}
if (request.getRemark() != null) {
document.setRemark(trimToNull(request.getRemark()));
}
boolean result = request.getId() == null ? save(document) : updateById(document); boolean result = request.getId() == null ? save(document) : updateById(document);
log.info("RagDocumentServiceImpl.saveOrUpdate success, requestId={}, savedId={}, result={}", log.info("RagDocumentServiceImpl.saveOrUpdate success, requestId={}, savedId={}, result={}",
@@ -167,7 +140,7 @@ public class RagDocumentServiceImpl extends ServiceImpl<RagDocumentMapper, RagDo
document.setRemark(trimToNull(request.getRemark())); document.setRemark(trimToNull(request.getRemark()));
save(document); save(document);
results.add(RagDocumentResponse.fromEntity(document)); results.add(ragDocumentFactory.toResponse(document));
} }
if (!results.isEmpty()) { if (!results.isEmpty()) {
@@ -211,10 +184,37 @@ public class RagDocumentServiceImpl extends ServiceImpl<RagDocumentMapper, RagDo
} }
} }
private List<RagDocumentResponse> toResponses(List<RagDocument> documents) { /**
return documents.stream() * 更新场景下需要保留旧值,因此这里仅覆盖请求中明确传入的字段。
.map(RagDocumentResponse::fromEntity) */
.toList(); private void applyRequestToDocument(RagDocument document, RagDocumentSaveRequest request) {
if (request.getStoreId() != null) {
document.setStoreId(request.getStoreId());
}
if (request.getAttachmentId() != null) {
document.setAttachmentId(request.getAttachmentId());
}
if (StringUtils.hasText(request.getDocumentTitle())) {
document.setDocumentTitle(trimToNull(request.getDocumentTitle()));
}
if (request.getDocumentSummary() != null) {
document.setDocumentSummary(trimToNull(request.getDocumentSummary()));
}
if (StringUtils.hasText(request.getParseStatus())) {
document.setParseStatus(trimToNull(request.getParseStatus()));
}
if (StringUtils.hasText(request.getIndexStatus())) {
document.setIndexStatus(trimToNull(request.getIndexStatus()));
}
if (request.getEnabled() != null) {
document.setEnabled(request.getEnabled());
}
if (request.getErrorMessage() != null) {
document.setErrorMessage(trimToNull(request.getErrorMessage()));
}
if (request.getRemark() != null) {
document.setRemark(trimToNull(request.getRemark()));
}
} }
private String resolveSourceType(String sourceType) { private String resolveSourceType(String sourceType) {

View File

@@ -12,11 +12,12 @@ import com.bruce.rag.dto.response.RagStoreResponse;
import com.bruce.rag.entity.RagStore; import com.bruce.rag.entity.RagStore;
import com.bruce.rag.enums.RagIndexStatusEnum; import com.bruce.rag.enums.RagIndexStatusEnum;
import com.bruce.rag.enums.RagParseStatusEnum; import com.bruce.rag.enums.RagParseStatusEnum;
import com.bruce.rag.factory.RagStoreFactory;
import com.bruce.rag.mapper.RagStoreMapper; import com.bruce.rag.mapper.RagStoreMapper;
import com.bruce.rag.service.IRagDocumentService; import com.bruce.rag.service.IRagDocumentService;
import com.bruce.rag.service.IRagStoreService; import com.bruce.rag.service.IRagStoreService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@@ -26,15 +27,16 @@ import java.util.Objects;
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor
public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> implements IRagStoreService { public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> implements IRagStoreService {
@Autowired private final IRagDocumentService ragDocumentService;
private IRagDocumentService ragDocumentService; private final RagStoreFactory ragStoreFactory;
@Override @Override
public List<RagStoreResponse> listResponses() { public List<RagStoreResponse> listResponses() {
log.info("RagStoreServiceImpl.listResponses start"); log.info("RagStoreServiceImpl.listResponses start");
List<RagStoreResponse> responses = toResponses(list()); List<RagStoreResponse> responses = ragStoreFactory.toResponses(list());
log.info("RagStoreServiceImpl.listResponses success, count={}", responses.size()); log.info("RagStoreServiceImpl.listResponses success, count={}", responses.size());
return responses; return responses;
} }
@@ -43,7 +45,7 @@ public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> i
public List<RagStoreResponse> query(RagStoreQueryRequest request) { public List<RagStoreResponse> query(RagStoreQueryRequest request) {
log.info("RagStoreServiceImpl.query start, request={}", request); log.info("RagStoreServiceImpl.query start, request={}", request);
RagStoreQueryRequest queryRequest = request == null ? new RagStoreQueryRequest() : request; RagStoreQueryRequest queryRequest = request == null ? new RagStoreQueryRequest() : request;
List<RagStoreResponse> responses = toResponses(lambdaQuery() List<RagStoreResponse> responses = ragStoreFactory.toResponses(lambdaQuery()
.eq(StringUtils.hasText(queryRequest.getStoreCode()), RagStore::getStoreCode, queryRequest.getStoreCode()) .eq(StringUtils.hasText(queryRequest.getStoreCode()), RagStore::getStoreCode, queryRequest.getStoreCode())
.like(StringUtils.hasText(queryRequest.getStoreName()), RagStore::getStoreName, queryRequest.getStoreName()) .like(StringUtils.hasText(queryRequest.getStoreName()), RagStore::getStoreName, queryRequest.getStoreName())
.eq(StringUtils.hasText(queryRequest.getStatus()), RagStore::getStatus, queryRequest.getStatus()) .eq(StringUtils.hasText(queryRequest.getStatus()), RagStore::getStatus, queryRequest.getStatus())
@@ -56,7 +58,7 @@ public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> i
@Override @Override
public RagStoreResponse getResponseById(Long id) { public RagStoreResponse getResponseById(Long id) {
log.info("RagStoreServiceImpl.getResponseById start, id={}", id); log.info("RagStoreServiceImpl.getResponseById start, id={}", id);
RagStoreResponse response = RagStoreResponse.fromEntity(getById(id)); RagStoreResponse response = ragStoreFactory.toResponse(getById(id));
log.info("RagStoreServiceImpl.getResponseById success, id={}, found={}", id, response != null); log.info("RagStoreServiceImpl.getResponseById success, id={}, found={}", id, response != null);
return response; return response;
} }
@@ -132,7 +134,10 @@ public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> i
throw new IllegalArgumentException("知识库编码已存在: " + request.getStoreCode().trim()); throw new IllegalArgumentException("知识库编码已存在: " + request.getStoreCode().trim());
} }
RagStore ragStore = buildEntity(request); RagStore ragStore = ragStoreFactory.toEntity(request);
if (!StringUtils.hasText(ragStore.getStatus())) {
ragStore.setStatus(EnableStatusEnum.ENABLED.getLabel());
}
boolean result = super.saveOrUpdate(ragStore); boolean result = super.saveOrUpdate(ragStore);
log.info("RagStoreServiceImpl.saveOrUpdate success, requestId={}, savedId={}, storeCode={}, result={}", log.info("RagStoreServiceImpl.saveOrUpdate success, requestId={}, savedId={}, storeCode={}, result={}",
request.getId(), ragStore.getId(), ragStore.getStoreCode(), result); request.getId(), ragStore.getId(), ragStore.getStoreCode(), result);
@@ -154,29 +159,4 @@ public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> i
request.getId(), request.getStoreCode(), request.getStoreName()); request.getId(), request.getStoreCode(), request.getStoreName());
} }
public RagStore buildEntity(RagStoreSaveRequest request) {
RagStore ragStore = new RagStore();
ragStore.setId(request.getId());
ragStore.setStoreCode(request.getStoreCode().trim());
ragStore.setStoreName(request.getStoreName().trim());
ragStore.setDescription(trimToNull(request.getDescription()));
ragStore.setStatus(StringUtils.hasText(request.getStatus())
? request.getStatus().trim()
: EnableStatusEnum.ENABLED.getLabel());
ragStore.setRemark(trimToNull(request.getRemark()));
return ragStore;
}
private List<RagStoreResponse> toResponses(List<RagStore> stores) {
return stores.stream()
.map(RagStoreResponse::fromEntity)
.toList();
}
private String trimToNull(String value) {
if (!StringUtils.hasText(value)) {
return null;
}
return value.trim();
}
} }

View File

@@ -0,0 +1,38 @@
package com.bruce.rag.vo;
import com.bruce.common.constant.CommonConsts;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
/**
* 知识工作台中的文档摘要视图。
*/
@Data
@Schema(description = "知识工作台文档摘要")
public class KnowledgeWorkspaceDocumentVO {
@Schema(description = "文档ID")
@JsonSerialize(using = ToStringSerializer.class)
private Long documentId;
@Schema(description = "文档标题")
private String documentTitle;
@Schema(description = "解析状态")
private String parseStatus;
@Schema(description = "索引状态")
private String indexStatus;
@Schema(description = "是否启用")
private Boolean enabled;
@Schema(description = "最近更新时间")
@JsonFormat(pattern = CommonConsts.DATE_FORMAT_LONG_STR, timezone = CommonConsts.TIME_ZONE_GMT8)
private Date updateTime;
}

View File

@@ -0,0 +1,88 @@
package com.bruce.rag.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 知识工作台聚合视图,供前端知识资产工作台直接消费。
*/
@Data
@Schema(description = "知识工作台聚合视图")
public class KnowledgeWorkspaceVO {
@Schema(description = "知识库ID")
@JsonSerialize(using = ToStringSerializer.class)
private Long storeId;
@Schema(description = "知识库编码")
private String storeCode;
@Schema(description = "知识库名称")
private String storeName;
@Schema(description = "知识库描述")
private String description;
@Schema(description = "知识库状态")
private String status;
@Schema(description = "备注")
private String remark;
@Schema(description = "文档总数")
private Integer documentCount;
@Schema(description = "已解析文档数")
private Integer parsedDocumentCount;
@Schema(description = "解析失败文档数")
private Integer parseFailedDocumentCount;
@Schema(description = "已索引文档数")
private Integer indexedDocumentCount;
@Schema(description = "待索引文档数")
private Integer pendingIndexDocumentCount;
@Schema(description = "健康度百分比")
private Integer healthScore;
@Schema(description = "Embedding 模型ID")
@JsonSerialize(using = ToStringSerializer.class)
private Long embeddingModelId;
@Schema(description = "Embedding 维度")
private Integer embeddingDimension;
@Schema(description = "切片策略")
private Integer chunkStrategy;
@Schema(description = "切片长度")
private Integer chunkSize;
@Schema(description = "切片重叠长度")
private Integer chunkOverlap;
@Schema(description = "索引版本")
private Integer indexVersion;
@Schema(description = "切片总数")
private Long chunkCount;
@Schema(description = "向量总数")
private Long embeddingCount;
@Schema(description = "待处理任务数")
private Integer pendingTaskCount;
@Schema(description = "发布影响提示")
private String publishImpact;
@Schema(description = "文档摘要列表")
private List<KnowledgeWorkspaceDocumentVO> documents = new ArrayList<>();
}

View File

@@ -9,6 +9,7 @@ import com.bruce.rag.dto.request.RagDocumentSaveRequest;
import com.bruce.rag.entity.RagDocument; import com.bruce.rag.entity.RagDocument;
import com.bruce.rag.enums.RagIndexStatusEnum; import com.bruce.rag.enums.RagIndexStatusEnum;
import com.bruce.rag.enums.RagParseStatusEnum; import com.bruce.rag.enums.RagParseStatusEnum;
import com.bruce.rag.factory.RagDocumentFactory;
import com.bruce.rag.service.IRagDocumentAutoParseService; import com.bruce.rag.service.IRagDocumentAutoParseService;
import com.bruce.rag.service.impl.RagDocumentServiceImpl; import com.bruce.rag.service.impl.RagDocumentServiceImpl;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -42,6 +43,9 @@ class RagDocumentServiceImplTests {
@Mock @Mock
private IRagDocumentAutoParseService ragDocumentAutoParseService; private IRagDocumentAutoParseService ragDocumentAutoParseService;
@Spy
private RagDocumentFactory ragDocumentFactory;
@Test @Test
void batchUploadShouldUseRagSourceTypeAndStoreIdAsSourceId() { void batchUploadShouldUseRagSourceTypeAndStoreIdAsSourceId() {
MockMultipartFile file = new MockMultipartFile( MockMultipartFile file = new MockMultipartFile(

View File

@@ -3,8 +3,11 @@ package com.bruce.rag;
import com.bruce.common.enums.EnableStatusEnum; import com.bruce.common.enums.EnableStatusEnum;
import com.bruce.rag.dto.request.RagStoreSaveRequest; import com.bruce.rag.dto.request.RagStoreSaveRequest;
import com.bruce.rag.entity.RagStore; import com.bruce.rag.entity.RagStore;
import com.bruce.rag.factory.RagStoreFactory;
import com.bruce.rag.service.IRagDocumentService;
import com.bruce.rag.service.impl.RagStoreServiceImpl; import com.bruce.rag.service.impl.RagStoreServiceImpl;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -12,9 +15,12 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
class RagStoreSaveValidationTests { class RagStoreSaveValidationTests {
private final IRagDocumentService ragDocumentService = Mockito.mock(IRagDocumentService.class);
private final RagStoreFactory ragStoreFactory = new RagStoreFactory();
@Test @Test
void saveShouldRejectBlankStoreCode() { void saveShouldRejectBlankStoreCode() {
RagStoreServiceImpl service = new RagStoreServiceImpl(); RagStoreServiceImpl service = new RagStoreServiceImpl(ragDocumentService, ragStoreFactory);
RagStoreSaveRequest request = new RagStoreSaveRequest(); RagStoreSaveRequest request = new RagStoreSaveRequest();
request.setStoreName("产品制度库"); request.setStoreName("产品制度库");
@@ -23,7 +29,7 @@ class RagStoreSaveValidationTests {
@Test @Test
void saveShouldRejectBlankStoreName() { void saveShouldRejectBlankStoreName() {
RagStoreServiceImpl service = new RagStoreServiceImpl(); RagStoreServiceImpl service = new RagStoreServiceImpl(ragDocumentService, ragStoreFactory);
RagStoreSaveRequest request = new RagStoreSaveRequest(); RagStoreSaveRequest request = new RagStoreSaveRequest();
request.setStoreCode("PROD_DOC"); request.setStoreCode("PROD_DOC");
@@ -32,7 +38,7 @@ class RagStoreSaveValidationTests {
@Test @Test
void saveShouldAcceptMinimalValidRequest() { void saveShouldAcceptMinimalValidRequest() {
RagStoreServiceImpl service = new RagStoreServiceImpl(); RagStoreServiceImpl service = new RagStoreServiceImpl(ragDocumentService, ragStoreFactory);
RagStoreSaveRequest request = new RagStoreSaveRequest(); RagStoreSaveRequest request = new RagStoreSaveRequest();
request.setStoreCode("PROD_DOC"); request.setStoreCode("PROD_DOC");
request.setStoreName("产品制度库"); request.setStoreName("产品制度库");
@@ -42,12 +48,14 @@ class RagStoreSaveValidationTests {
@Test @Test
void saveShouldDefaultStatusToEnabledEnumLabel() { void saveShouldDefaultStatusToEnabledEnumLabel() {
RagStoreServiceImpl service = new RagStoreServiceImpl();
RagStoreSaveRequest request = new RagStoreSaveRequest(); RagStoreSaveRequest request = new RagStoreSaveRequest();
request.setStoreCode("PROD_DOC"); request.setStoreCode("PROD_DOC");
request.setStoreName("产品制度库"); request.setStoreName("产品制度库");
RagStore ragStore = service.buildEntity(request); RagStore ragStore = ragStoreFactory.toEntity(request);
if (ragStore.getStatus() == null) {
ragStore.setStatus(EnableStatusEnum.ENABLED.getLabel());
}
assertEquals(EnableStatusEnum.ENABLED.getLabel(), ragStore.getStatus()); assertEquals(EnableStatusEnum.ENABLED.getLabel(), ragStore.getStatus());
} }

View File

@@ -0,0 +1,129 @@
package com.bruce.rag.factory;
import com.bruce.rag.dto.request.RagDocumentSaveRequest;
import com.bruce.rag.dto.request.RagStoreSaveRequest;
import com.bruce.rag.dto.response.RagDocumentResponse;
import com.bruce.rag.dto.response.RagStoreResponse;
import com.bruce.rag.entity.RagDocument;
import com.bruce.rag.entity.RagStore;
import org.junit.jupiter.api.Test;
import java.util.Date;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
class RagFactoryTests {
private final RagStoreFactory ragStoreFactory = new RagStoreFactory();
private final RagDocumentFactory ragDocumentFactory = new RagDocumentFactory();
@Test
void ragStoreFactoryShouldTrimAndBuildEntity() {
RagStoreSaveRequest request = new RagStoreSaveRequest();
request.setId(1001L);
request.setStoreCode(" PROD_DOC ");
request.setStoreName(" 产品制度库 ");
request.setDescription(" 产品制度、业务规范、流程材料 ");
request.setStatus(" 启用 ");
request.setRemark(" 核心制度库 ");
RagStore entity = ragStoreFactory.toEntity(request);
assertNotNull(entity);
assertEquals(1001L, entity.getId());
assertEquals("PROD_DOC", entity.getStoreCode());
assertEquals("产品制度库", entity.getStoreName());
assertEquals("产品制度、业务规范、流程材料", entity.getDescription());
assertEquals("启用", entity.getStatus());
assertEquals("核心制度库", entity.getRemark());
}
@Test
void ragStoreFactoryShouldConvertEntityToResponse() {
RagStore entity = new RagStore();
entity.setId(1001L);
entity.setStoreCode("PROD_DOC");
entity.setStoreName("产品制度库");
entity.setDescription("产品制度、业务规范、流程材料");
entity.setStatus("启用");
entity.setRemark("核心制度库");
entity.setCreateTime(new Date(1747816496000L));
entity.setUpdateTime(new Date(1747820096000L));
RagStoreResponse response = ragStoreFactory.toResponse(entity);
assertNotNull(response);
assertEquals(1001L, response.getId());
assertEquals("PROD_DOC", response.getStoreCode());
assertEquals("产品制度库", response.getStoreName());
assertEquals("产品制度、业务规范、流程材料", response.getDescription());
assertEquals("启用", response.getStatus());
assertEquals("核心制度库", response.getRemark());
assertEquals(new Date(1747816496000L), response.getCreateTime());
assertEquals(new Date(1747820096000L), response.getUpdateTime());
}
@Test
void ragDocumentFactoryShouldApplyEditableFieldsToEntity() {
RagDocumentSaveRequest request = new RagDocumentSaveRequest();
request.setId(3003L);
request.setStoreId(1001L);
request.setAttachmentId(2002L);
request.setDocumentTitle(" 新标题 ");
request.setDocumentSummary(" 新摘要 ");
request.setParseStatus(" PARSED ");
request.setIndexStatus(" INDEXED ");
request.setEnabled(false);
request.setErrorMessage(" 已修复 ");
request.setRemark(" 备注信息 ");
RagDocument entity = ragDocumentFactory.toEntity(request);
assertNotNull(entity);
assertEquals(3003L, entity.getId());
assertEquals(1001L, entity.getStoreId());
assertEquals(2002L, entity.getAttachmentId());
assertEquals("新标题", entity.getDocumentTitle());
assertEquals("新摘要", entity.getDocumentSummary());
assertEquals("PARSED", entity.getParseStatus());
assertEquals("INDEXED", entity.getIndexStatus());
assertEquals(false, entity.getEnabled());
assertEquals("已修复", entity.getErrorMessage());
assertEquals("备注信息", entity.getRemark());
}
@Test
void ragDocumentFactoryShouldConvertEntityToResponse() {
RagDocument entity = new RagDocument();
entity.setId(3003L);
entity.setStoreId(1001L);
entity.setAttachmentId(2002L);
entity.setDocumentTitle("people_profiles.txt");
entity.setDocumentSummary("测试人员信息");
entity.setParseStatus("PARSED");
entity.setIndexStatus("INDEXED");
entity.setEnabled(true);
entity.setErrorMessage(null);
entity.setRemark("测试文档");
entity.setCreateTime(new Date(1747816496000L));
entity.setUpdateTime(new Date(1747820096000L));
RagDocumentResponse response = ragDocumentFactory.toResponse(entity);
assertNotNull(response);
assertEquals(3003L, response.getId());
assertEquals(1001L, response.getStoreId());
assertEquals(2002L, response.getAttachmentId());
assertEquals("people_profiles.txt", response.getDocumentTitle());
assertEquals("测试人员信息", response.getDocumentSummary());
assertEquals("PARSED", response.getParseStatus());
assertEquals("INDEXED", response.getIndexStatus());
assertEquals(true, response.getEnabled());
assertNull(response.getErrorMessage());
assertEquals("测试文档", response.getRemark());
assertEquals(new Date(1747816496000L), response.getCreateTime());
assertEquals(new Date(1747820096000L), response.getUpdateTime());
}
}

View File

@@ -0,0 +1,120 @@
package com.bruce.rag.workspace;
import com.bruce.modelprovider.dto.response.RagStoreModelConfigResponse;
import com.bruce.modelprovider.service.IRagStoreModelConfigService;
import com.bruce.rag.dto.response.RagDocumentResponse;
import com.bruce.rag.dto.response.RagStoreResponse;
import com.bruce.rag.service.IRagChunkEmbeddingService;
import com.bruce.rag.service.IRagChunkService;
import com.bruce.rag.service.IRagDocumentParseResultService;
import com.bruce.rag.service.IRagDocumentService;
import com.bruce.rag.service.impl.KnowledgeWorkspaceServiceImpl;
import com.bruce.rag.vo.KnowledgeWorkspaceVO;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Date;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class KnowledgeWorkspaceServiceTests {
@Mock
private com.bruce.rag.service.IRagStoreService ragStoreService;
@Mock
private IRagDocumentService ragDocumentService;
@Mock
private IRagDocumentParseResultService ragDocumentParseResultService;
@Mock
private IRagChunkService ragChunkService;
@Mock
private IRagChunkEmbeddingService ragChunkEmbeddingService;
@Mock
private IRagStoreModelConfigService ragStoreModelConfigService;
@InjectMocks
private KnowledgeWorkspaceServiceImpl knowledgeWorkspaceService;
@Test
void getWorkspaceShouldAggregateStoreConfigAndDocumentHealth() {
RagStoreResponse store = new RagStoreResponse();
store.setId(1001L);
store.setStoreCode("PROD_DOC");
store.setStoreName("产品制度库");
store.setDescription("产品制度、业务规范、流程材料");
store.setStatus("启用");
store.setRemark("核心制度库");
RagDocumentResponse parsedIndexedDocument = createDocument(11L, true, "PARSED", "INDEXED", new Date(1747816496000L));
RagDocumentResponse parseFailedDocument = createDocument(12L, true, "FAILED", "FAILED", new Date(1747820096000L));
RagDocumentResponse pendingIndexDocument = createDocument(13L, true, "PARSED", "PENDING", new Date(1747823696000L));
RagStoreModelConfigResponse modelConfig = new RagStoreModelConfigResponse();
modelConfig.setStoreId(1001L);
modelConfig.setEmbeddingModelId(88L);
modelConfig.setEmbeddingDimension(1024);
modelConfig.setChunkStrategy(1);
modelConfig.setChunkSize(800);
modelConfig.setChunkOverlap(120);
modelConfig.setIndexVersion(14);
when(ragStoreService.getResponseById(1001L)).thenReturn(store);
when(ragDocumentService.query(org.mockito.ArgumentMatchers.any())).thenReturn(List.of(
parsedIndexedDocument,
parseFailedDocument,
pendingIndexDocument
));
when(ragStoreModelConfigService.getByStoreId(1001L)).thenReturn(modelConfig);
when(ragChunkService.count(org.mockito.ArgumentMatchers.any())).thenReturn(24L);
when(ragChunkEmbeddingService.count(org.mockito.ArgumentMatchers.any())).thenReturn(18L);
KnowledgeWorkspaceVO workspace = knowledgeWorkspaceService.getWorkspace(1001L);
assertNotNull(workspace);
assertEquals(1001L, workspace.getStoreId());
assertEquals("PROD_DOC", workspace.getStoreCode());
assertEquals("产品制度库", workspace.getStoreName());
assertEquals("启用", workspace.getStatus());
assertEquals(3, workspace.getDocumentCount());
assertEquals(2, workspace.getParsedDocumentCount());
assertEquals(1, workspace.getParseFailedDocumentCount());
assertEquals(1, workspace.getIndexedDocumentCount());
assertEquals(1, workspace.getPendingIndexDocumentCount());
assertEquals(66, workspace.getHealthScore());
assertEquals(88L, workspace.getEmbeddingModelId());
assertEquals(1024, workspace.getEmbeddingDimension());
assertEquals(1, workspace.getChunkStrategy());
assertEquals(800, workspace.getChunkSize());
assertEquals(120, workspace.getChunkOverlap());
assertEquals(14, workspace.getIndexVersion());
assertEquals(24L, workspace.getChunkCount());
assertEquals(18L, workspace.getEmbeddingCount());
assertEquals(1, workspace.getPendingTaskCount());
assertEquals("更新后需要重新验证 Workflow / Agent 的知识引用效果", workspace.getPublishImpact());
assertEquals(3, workspace.getDocuments().size());
}
private RagDocumentResponse createDocument(Long id, boolean enabled, String parseStatus, String indexStatus, Date updateTime) {
RagDocumentResponse response = new RagDocumentResponse();
response.setId(id);
response.setStoreId(1001L);
response.setEnabled(enabled);
response.setParseStatus(parseStatus);
response.setIndexStatus(indexStatus);
response.setDocumentTitle("文档-" + id);
response.setUpdateTime(updateTime);
return response;
}
}

View File

@@ -0,0 +1,20 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { getKnowledgeWorkspace } from '../knowledgeWorkspace';
import { get } from '../request';
vi.mock('../request', () => ({
get: vi.fn(),
}));
describe('knowledge workspace api', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('loads workspace aggregate by store id', () => {
getKnowledgeWorkspace('1001');
expect(get).toHaveBeenCalledWith('/knowledge/workspaces/1001');
});
});

View File

@@ -0,0 +1,40 @@
import { get } from './request';
export interface KnowledgeWorkspaceDocument {
documentId: string;
documentTitle?: string | null;
parseStatus?: string | null;
indexStatus?: string | null;
enabled?: boolean | null;
updateTime?: string | null;
}
export interface KnowledgeWorkspace {
storeId: string;
storeCode?: string | null;
storeName?: string | null;
description?: string | null;
status?: string | null;
remark?: string | null;
documentCount: number;
parsedDocumentCount: number;
parseFailedDocumentCount: number;
indexedDocumentCount: number;
pendingIndexDocumentCount: number;
healthScore: number;
embeddingModelId?: string | null;
embeddingDimension?: number | null;
chunkStrategy?: number | null;
chunkSize?: number | null;
chunkOverlap?: number | null;
indexVersion?: number | null;
chunkCount?: number | null;
embeddingCount?: number | null;
pendingTaskCount?: number | null;
publishImpact?: string | null;
documents: KnowledgeWorkspaceDocument[];
}
export function getKnowledgeWorkspace(storeId: string) {
return get<KnowledgeWorkspace>(`/knowledge/workspaces/${storeId}`);
}