diff --git a/frontend/src/api/__tests__/ragStores.spec.ts b/frontend/src/api/__tests__/ragStores.spec.ts new file mode 100644 index 0000000..6fd778f --- /dev/null +++ b/frontend/src/api/__tests__/ragStores.spec.ts @@ -0,0 +1,31 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + getRagStoreDocumentOverview, + getRagStoreOverview, + listRagStores, +} from '../ragStores'; +import { get, post } from '../request'; + +vi.mock('../request', () => ({ + get: vi.fn(), + post: vi.fn(), +})); + +describe('rag stores api', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('loads overview and document overview endpoints', () => { + getRagStoreOverview(); + getRagStoreDocumentOverview('1001'); + listRagStores(); + + expect(get).toHaveBeenCalledWith('/rag/store/overview'); + expect(get).toHaveBeenCalledWith('/rag/store/documentOverview', { + params: { storeId: '1001' }, + }); + expect(post).toHaveBeenCalledWith('/rag/store/list'); + }); +}); diff --git a/frontend/src/api/ragStores.ts b/frontend/src/api/ragStores.ts index aa3f3db..5b08a36 100644 --- a/frontend/src/api/ragStores.ts +++ b/frontend/src/api/ragStores.ts @@ -11,6 +11,23 @@ export interface RagStore { updateTime?: string | null; } +export interface RagStoreOverview { + totalStores: number; + totalDocuments: number; + totalChunks?: number | null; + retrievableStores: number; +} + +export interface RagStoreDocumentOverview { + storeId: string; + storeName?: string | null; + documentCount: number; + enabledDocumentCount: number; + parsedDocumentCount: number; + indexedDocumentCount: number; + lastUploadTime?: string | null; +} + export interface RagStoreQueryRequest { storeCode?: string; storeName?: string; @@ -33,6 +50,16 @@ export function getRagStoreById(id: string) { }); } +export function getRagStoreOverview() { + return get('/rag/store/overview'); +} + +export function getRagStoreDocumentOverview(storeId: string) { + return get('/rag/store/documentOverview', { + params: { storeId }, + }); +} + export function saveRagStore(data: RagStoreSaveRequest) { return post('/rag/store/save', data); } diff --git a/frontend/src/pages/RagStoresPage.vue b/frontend/src/pages/RagStoresPage.vue index 8cefdb0..6b5d53c 100644 --- a/frontend/src/pages/RagStoresPage.vue +++ b/frontend/src/pages/RagStoresPage.vue @@ -6,8 +6,12 @@ import { computed, onMounted, reactive, ref } from 'vue'; import { deleteRagStore, getRagStoreById, + getRagStoreDocumentOverview, + getRagStoreOverview, queryRagStores, saveRagStore, + type RagStoreDocumentOverview, + type RagStoreOverview, type RagStore, } from '@/api/ragStores'; @@ -19,6 +23,8 @@ const submitting = ref(false); const storeRows = ref([]); const activeStoreId = ref(null); const activeStore = ref(null); +const pageOverview = ref(null); +const activeStoreDocumentOverview = ref(null); const queryForm = reactive({ storeName: '', @@ -45,17 +51,30 @@ const editForm = reactive({ }); const overviewCards = computed(() => { - const totalStores = storeRows.value.length; - const retrievableStores = storeRows.value.filter((row) => row.status === '启用').length; + const totalStores = pageOverview.value?.totalStores ?? storeRows.value.length; + const totalDocuments = pageOverview.value?.totalDocuments ?? '-'; + const totalChunks = pageOverview.value?.totalChunks ?? '-'; + const retrievableStores = + pageOverview.value?.retrievableStores + ?? storeRows.value.filter((row) => row.status === '启用').length; return [ { label: '知识库总数', value: totalStores, hint: '当前已登记知识库' }, - { label: '文档总数', value: '-', hint: '待文档统计接口补充' }, - { label: '切片总数', value: '-', hint: '待切片统计接口补充' }, - { label: '可检索知识库数', value: retrievableStores, hint: '当前按启用状态暂代统计' }, + { label: '文档总数', value: totalDocuments, hint: '当前知识库已登记文档总量' }, + { label: '切片总数', value: totalChunks, hint: '当前未接入切片表,待后续能力补充' }, + { label: '可检索知识库数', value: retrievableStores, hint: '当前按启用状态统计' }, ]; }); +async function loadOverview() { + try { + const response = await getRagStoreOverview(); + pageOverview.value = response.data ?? null; + } catch { + pageOverview.value = null; + } +} + async function loadStores(preferredStoreId?: string | null) { loading.value = true; try { @@ -67,6 +86,7 @@ async function loadStores(preferredStoreId?: string | null) { if (storeRows.value.length === 0) { activeStoreId.value = null; activeStore.value = null; + activeStoreDocumentOverview.value = null; return; } @@ -80,6 +100,7 @@ async function loadStores(preferredStoreId?: string | null) { if (!targetId) { activeStoreId.value = null; activeStore.value = null; + activeStoreDocumentOverview.value = null; return; } await selectStore(targetId); @@ -92,8 +113,12 @@ async function selectStore(storeId: string) { activeStoreId.value = storeId; detailLoading.value = true; try { - const response = await getRagStoreById(storeId); - activeStore.value = response.data ?? null; + const [storeResponse, documentOverviewResponse] = await Promise.all([ + getRagStoreById(storeId), + getRagStoreDocumentOverview(storeId), + ]); + activeStore.value = storeResponse.data ?? null; + activeStoreDocumentOverview.value = documentOverviewResponse.data ?? null; } finally { detailLoading.value = false; } @@ -203,6 +228,7 @@ function getStatusTagType(status?: string | null) { } onMounted(() => { + loadOverview(); loadStores(); }); @@ -327,9 +353,25 @@ onMounted(() => {

文档概览

- 下一批接口补充 + 已对接后端聚合接口
- + + + {{ activeStoreDocumentOverview?.documentCount ?? 0 }} + + + {{ activeStoreDocumentOverview?.enabledDocumentCount ?? 0 }} + + + {{ activeStoreDocumentOverview?.parsedDocumentCount ?? 0 }} + + + {{ activeStoreDocumentOverview?.indexedDocumentCount ?? 0 }} + + + {{ activeStoreDocumentOverview?.lastUploadTime || '-' }} + +
diff --git a/frontend/src/pages/__tests__/RagStoresPage.spec.ts b/frontend/src/pages/__tests__/RagStoresPage.spec.ts index 9ab54e7..1450ae8 100644 --- a/frontend/src/pages/__tests__/RagStoresPage.spec.ts +++ b/frontend/src/pages/__tests__/RagStoresPage.spec.ts @@ -3,9 +3,27 @@ import ElementPlus from 'element-plus'; import { describe, expect, it, vi } from 'vitest'; import RagStoresPage from '../RagStoresPage.vue'; -import { getRagStoreById, queryRagStores, saveRagStore } from '@/api/ragStores'; +import { + getRagStoreById, + getRagStoreDocumentOverview, + getRagStoreOverview, + queryRagStores, + saveRagStore, +} from '@/api/ragStores'; vi.mock('@/api/ragStores', () => ({ + getRagStoreOverview: vi.fn(() => + Promise.resolve({ + resultcode: '0', + message: null, + data: { + totalStores: 2, + totalDocuments: 12, + totalChunks: null, + retrievableStores: 1, + }, + }), + ), queryRagStores: vi.fn((query?: { storeName?: string }) => { const rows = [ { @@ -60,6 +78,32 @@ vi.mock('@/api/ragStores', () => ({ }, }), ), + getRagStoreDocumentOverview: vi.fn((storeId: string) => + Promise.resolve({ + resultcode: '0', + message: null, + data: + storeId === '2' + ? { + storeId: '2', + storeName: 'FAQ知识库', + documentCount: 3, + enabledDocumentCount: 1, + parsedDocumentCount: 1, + indexedDocumentCount: 1, + lastUploadTime: '2026-05-21 11:12:00', + } + : { + storeId: '1', + storeName: '产品制度库', + documentCount: 9, + enabledDocumentCount: 8, + parsedDocumentCount: 6, + indexedDocumentCount: 5, + lastUploadTime: '2026-05-21 16:40:00', + }, + }), + ), saveRagStore: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })), deleteRagStore: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })), })); @@ -75,10 +119,16 @@ describe('RagStoresPage', () => { await flushPromises(); expect(wrapper.text()).toContain('知识库总数'); + expect(wrapper.text()).toContain('12'); expect(wrapper.text()).toContain('产品制度库'); expect(wrapper.text()).toContain('核心制度库'); + expect(wrapper.text()).toContain('文档总数'); + expect(wrapper.text()).toContain('最近上传时间'); + expect(wrapper.text()).toContain('2026-05-21 16:40:00'); + expect(getRagStoreOverview).toHaveBeenCalled(); expect(queryRagStores).toHaveBeenCalled(); expect(getRagStoreById).toHaveBeenCalledWith('1'); + expect(getRagStoreDocumentOverview).toHaveBeenCalledWith('1'); }); it('filters stores by name and updates detail when a store is selected', async () => { @@ -103,7 +153,9 @@ describe('RagStoresPage', () => { await flushPromises(); expect(getRagStoreById).toHaveBeenLastCalledWith('2'); + expect(getRagStoreDocumentOverview).toHaveBeenLastCalledWith('2'); expect(wrapper.text()).toContain('FAQ 场景知识'); + expect(wrapper.text()).toContain('3'); }); it('submits create form through backend api', async () => { diff --git a/src/main/java/com/bruce/rag/controller/RagStoreController.java b/src/main/java/com/bruce/rag/controller/RagStoreController.java index eb50853..e271654 100644 --- a/src/main/java/com/bruce/rag/controller/RagStoreController.java +++ b/src/main/java/com/bruce/rag/controller/RagStoreController.java @@ -3,6 +3,8 @@ package com.bruce.rag.controller; import com.bruce.common.domain.model.RequestResult; import com.bruce.rag.dto.request.RagStoreQueryRequest; import com.bruce.rag.dto.request.RagStoreSaveRequest; +import com.bruce.rag.dto.response.RagStoreDocumentOverviewResponse; +import com.bruce.rag.dto.response.RagStoreOverviewResponse; import com.bruce.rag.dto.response.RagStoreResponse; import com.bruce.rag.service.IRagStoreService; import io.swagger.v3.oas.annotations.Operation; @@ -54,6 +56,26 @@ public class RagStoreController { return RequestResult.success(response); } + @Operation(summary = "查询知识库总览") + @GetMapping("/overview") + public RequestResult overview() { + log.info("RagStoreController.overview start"); + RagStoreOverviewResponse response = ragStoreService.getOverview(); + log.info("RagStoreController.overview success, totalStores={}, totalDocuments={}", + response.getTotalStores(), response.getTotalDocuments()); + return RequestResult.success(response); + } + + @Operation(summary = "查询知识库文档概览") + @GetMapping("/documentOverview") + public RequestResult documentOverview(@RequestParam("storeId") Long storeId) { + log.info("RagStoreController.documentOverview start, storeId={}", storeId); + RagStoreDocumentOverviewResponse response = ragStoreService.getDocumentOverview(storeId); + log.info("RagStoreController.documentOverview success, storeId={}, documentCount={}", + storeId, response.getDocumentCount()); + return RequestResult.success(response); + } + @Operation(summary = "新增或修改知识库") @PostMapping("/save") public RequestResult saveOrUpdate(@RequestBody RagStoreSaveRequest request) { diff --git a/src/main/java/com/bruce/rag/dto/response/RagStoreDocumentOverviewResponse.java b/src/main/java/com/bruce/rag/dto/response/RagStoreDocumentOverviewResponse.java new file mode 100644 index 0000000..debb5e4 --- /dev/null +++ b/src/main/java/com/bruce/rag/dto/response/RagStoreDocumentOverviewResponse.java @@ -0,0 +1,38 @@ +package com.bruce.rag.dto.response; + +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 = "RAG知识库文档概览响应") +public class RagStoreDocumentOverviewResponse { + + @Schema(description = "知识库ID") + @JsonSerialize(using = ToStringSerializer.class) + private Long storeId; + + @Schema(description = "知识库名称") + private String storeName; + + @Schema(description = "文档总数") + private Integer documentCount; + + @Schema(description = "启用文档数") + private Integer enabledDocumentCount; + + @Schema(description = "已解析文档数") + private Integer parsedDocumentCount; + + @Schema(description = "已索引文档数") + private Integer indexedDocumentCount; + + @Schema(description = "最近上传时间") + @JsonFormat(pattern = CommonConsts.DATE_FORMAT_LONG_STR, timezone = CommonConsts.TIME_ZONE_GMT8) + private Date lastUploadTime; +} diff --git a/src/main/java/com/bruce/rag/dto/response/RagStoreOverviewResponse.java b/src/main/java/com/bruce/rag/dto/response/RagStoreOverviewResponse.java new file mode 100644 index 0000000..80eac58 --- /dev/null +++ b/src/main/java/com/bruce/rag/dto/response/RagStoreOverviewResponse.java @@ -0,0 +1,21 @@ +package com.bruce.rag.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "RAG知识库总览响应") +public class RagStoreOverviewResponse { + + @Schema(description = "知识库总数") + private Integer totalStores; + + @Schema(description = "文档总数") + private Integer totalDocuments; + + @Schema(description = "切片总数") + private Integer totalChunks; + + @Schema(description = "可检索知识库数") + private Integer retrievableStores; +} diff --git a/src/main/java/com/bruce/rag/service/IRagStoreService.java b/src/main/java/com/bruce/rag/service/IRagStoreService.java index 617d43c..1a6df5f 100644 --- a/src/main/java/com/bruce/rag/service/IRagStoreService.java +++ b/src/main/java/com/bruce/rag/service/IRagStoreService.java @@ -3,6 +3,8 @@ package com.bruce.rag.service; import com.baomidou.mybatisplus.extension.service.IService; import com.bruce.rag.dto.request.RagStoreQueryRequest; import com.bruce.rag.dto.request.RagStoreSaveRequest; +import com.bruce.rag.dto.response.RagStoreDocumentOverviewResponse; +import com.bruce.rag.dto.response.RagStoreOverviewResponse; import com.bruce.rag.dto.response.RagStoreResponse; import com.bruce.rag.entity.RagStore; @@ -16,5 +18,9 @@ public interface IRagStoreService extends IService { RagStoreResponse getResponseById(Long id); + RagStoreOverviewResponse getOverview(); + + RagStoreDocumentOverviewResponse getDocumentOverview(Long storeId); + boolean saveOrUpdate(RagStoreSaveRequest request); } diff --git a/src/main/java/com/bruce/rag/service/impl/RagStoreServiceImpl.java b/src/main/java/com/bruce/rag/service/impl/RagStoreServiceImpl.java index 69f3ce0..ef51381 100644 --- a/src/main/java/com/bruce/rag/service/impl/RagStoreServiceImpl.java +++ b/src/main/java/com/bruce/rag/service/impl/RagStoreServiceImpl.java @@ -1,22 +1,36 @@ package com.bruce.rag.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bruce.common.enums.EnableStatusEnum; +import com.bruce.rag.dto.request.RagDocumentQueryRequest; import com.bruce.rag.dto.request.RagStoreQueryRequest; import com.bruce.rag.dto.request.RagStoreSaveRequest; +import com.bruce.rag.dto.response.RagDocumentResponse; +import com.bruce.rag.dto.response.RagStoreDocumentOverviewResponse; +import com.bruce.rag.dto.response.RagStoreOverviewResponse; import com.bruce.rag.dto.response.RagStoreResponse; import com.bruce.rag.entity.RagStore; +import com.bruce.rag.enums.RagIndexStatusEnum; +import com.bruce.rag.enums.RagParseStatusEnum; import com.bruce.rag.mapper.RagStoreMapper; +import com.bruce.rag.service.IRagDocumentService; import com.bruce.rag.service.IRagStoreService; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import java.util.Comparator; import java.util.List; +import java.util.Objects; @Slf4j @Service public class RagStoreServiceImpl extends ServiceImpl implements IRagStoreService { + @Autowired + private IRagDocumentService ragDocumentService; + @Override public List listResponses() { log.info("RagStoreServiceImpl.listResponses start"); @@ -47,6 +61,62 @@ public class RagStoreServiceImpl extends ServiceImpl i return response; } + @Override + public RagStoreOverviewResponse getOverview() { + log.info("RagStoreServiceImpl.getOverview start"); + List stores = list(); + List documents = ragDocumentService.listResponses(); + + RagStoreOverviewResponse response = new RagStoreOverviewResponse(); + response.setTotalStores(stores.size()); + response.setTotalDocuments(documents.size()); + response.setTotalChunks(null); + response.setRetrievableStores((int) stores.stream() + .filter(store -> EnableStatusEnum.ENABLED.getLabel().equals(store.getStatus())) + .count()); + log.info("RagStoreServiceImpl.getOverview success, totalStores={}, totalDocuments={}, retrievableStores={}", + response.getTotalStores(), response.getTotalDocuments(), response.getRetrievableStores()); + return response; + } + + @Override + public RagStoreDocumentOverviewResponse getDocumentOverview(Long storeId) { + log.info("RagStoreServiceImpl.getDocumentOverview start, storeId={}", storeId); + if (storeId == null) { + throw new IllegalArgumentException("知识库ID不能为空"); + } + RagStore store = getById(storeId); + if (store == null) { + throw new IllegalArgumentException("知识库不存在,ID: " + storeId); + } + + RagDocumentQueryRequest request = new RagDocumentQueryRequest(); + request.setStoreId(storeId); + List documents = ragDocumentService.query(request); + + RagStoreDocumentOverviewResponse response = new RagStoreDocumentOverviewResponse(); + response.setStoreId(storeId); + response.setStoreName(store.getStoreName()); + response.setDocumentCount(documents.size()); + response.setEnabledDocumentCount((int) documents.stream() + .filter(document -> Boolean.TRUE.equals(document.getEnabled())) + .count()); + response.setParsedDocumentCount((int) documents.stream() + .filter(document -> RagParseStatusEnum.PARSED.name().equals(document.getParseStatus())) + .count()); + response.setIndexedDocumentCount((int) documents.stream() + .filter(document -> RagIndexStatusEnum.INDEXED.name().equals(document.getIndexStatus())) + .count()); + response.setLastUploadTime(documents.stream() + .map(RagDocumentResponse::getCreateTime) + .filter(Objects::nonNull) + .max(Comparator.naturalOrder()) + .orElse(null)); + log.info("RagStoreServiceImpl.getDocumentOverview success, storeId={}, documentCount={}", + storeId, response.getDocumentCount()); + return response; + } + @Override public boolean saveOrUpdate(RagStoreSaveRequest request) { log.info("RagStoreServiceImpl.saveOrUpdate start, request={}", request); @@ -62,13 +132,7 @@ public class RagStoreServiceImpl extends ServiceImpl i throw new IllegalArgumentException("知识库编码已存在: " + request.getStoreCode().trim()); } - 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() : "启用"); - ragStore.setRemark(trimToNull(request.getRemark())); + RagStore ragStore = buildEntity(request); boolean result = super.saveOrUpdate(ragStore); log.info("RagStoreServiceImpl.saveOrUpdate success, requestId={}, savedId={}, storeCode={}, result={}", request.getId(), ragStore.getId(), ragStore.getStoreCode(), result); @@ -90,6 +154,19 @@ public class RagStoreServiceImpl extends ServiceImpl i 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 toResponses(List stores) { return stores.stream() .map(RagStoreResponse::fromEntity) diff --git a/src/test/java/com/bruce/rag/RagComponentStructureTests.java b/src/test/java/com/bruce/rag/RagComponentStructureTests.java index 9926ae7..d7c9bdc 100644 --- a/src/test/java/com/bruce/rag/RagComponentStructureTests.java +++ b/src/test/java/com/bruce/rag/RagComponentStructureTests.java @@ -10,6 +10,8 @@ import com.bruce.rag.controller.RagStoreController; import com.bruce.rag.dto.request.RagDocumentQueryRequest; import com.bruce.rag.dto.request.RagStoreQueryRequest; import com.bruce.rag.dto.request.RagStoreSaveRequest; +import com.bruce.rag.dto.response.RagStoreDocumentOverviewResponse; +import com.bruce.rag.dto.response.RagStoreOverviewResponse; import com.bruce.rag.dto.response.RagDocumentResponse; import com.bruce.rag.dto.response.RagStoreResponse; import com.bruce.rag.entity.RagDocument; @@ -21,12 +23,14 @@ import com.bruce.rag.service.IRagStoreService; import com.bruce.rag.service.impl.RagDocumentServiceImpl; import com.bruce.rag.service.impl.RagStoreServiceImpl; import org.junit.jupiter.api.Test; +import org.springframework.web.bind.annotation.PostMapping; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; class RagComponentStructureTests { @@ -46,11 +50,15 @@ class RagComponentStructureTests { Method storeListMethod = RagStoreController.class.getMethod("list"); Method storeQueryMethod = RagStoreController.class.getMethod("query", RagStoreQueryRequest.class); Method storeDetailMethod = RagStoreController.class.getMethod("getById", Long.class); + Method storeOverviewMethod = RagStoreController.class.getMethod("overview"); + Method storeDocumentOverviewMethod = RagStoreController.class.getMethod("documentOverview", Long.class); Method storeSaveMethod = RagStoreController.class.getMethod("saveOrUpdate", RagStoreSaveRequest.class); Method storeDeleteMethod = RagStoreController.class.getMethod("deleteById", Long.class); Method storeResponseListMethod = IRagStoreService.class.getMethod("listResponses"); Method storeServiceQueryMethod = IRagStoreService.class.getMethod("query", RagStoreQueryRequest.class); Method storeServiceDetailMethod = IRagStoreService.class.getMethod("getResponseById", Long.class); + Method storeServiceOverviewMethod = IRagStoreService.class.getMethod("getOverview"); + Method storeServiceDocumentOverviewMethod = IRagStoreService.class.getMethod("getDocumentOverview", Long.class); Method storeServiceSaveMethod = IRagStoreService.class.getMethod("saveOrUpdate", RagStoreSaveRequest.class); Method documentListMethod = RagDocumentController.class.getMethod("list"); @@ -61,16 +69,22 @@ class RagComponentStructureTests { assertEquals(RequestResult.class, storeListMethod.getReturnType()); assertEquals(RequestResult.class, storeQueryMethod.getReturnType()); assertEquals(RequestResult.class, storeDetailMethod.getReturnType()); + assertEquals(RequestResult.class, storeOverviewMethod.getReturnType()); + assertEquals(RequestResult.class, storeDocumentOverviewMethod.getReturnType()); assertEquals(RequestResult.class, storeSaveMethod.getReturnType()); assertEquals(RequestResult.class, storeDeleteMethod.getReturnType()); assertEquals(List.class, storeServiceQueryMethod.getReturnType()); assertEquals(RagStoreResponse.class, storeServiceDetailMethod.getReturnType()); + assertEquals(RagStoreOverviewResponse.class, storeServiceOverviewMethod.getReturnType()); + assertEquals(RagStoreDocumentOverviewResponse.class, storeServiceDocumentOverviewMethod.getReturnType()); assertEquals(boolean.class, storeServiceSaveMethod.getReturnType()); assertTrue(storeResponseListMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse")); assertTrue(storeServiceQueryMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse")); assertTrue(storeListMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse")); assertTrue(storeQueryMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse")); assertTrue(storeDetailMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse")); + assertTrue(storeOverviewMethod.getGenericReturnType().getTypeName().contains("RagStoreOverviewResponse")); + assertTrue(storeDocumentOverviewMethod.getGenericReturnType().getTypeName().contains("RagStoreDocumentOverviewResponse")); assertEquals(RagStoreResponse.class, RagStoreResponse.class.getMethod("fromEntity", RagStore.class).getReturnType()); assertEquals(RequestResult.class, documentListMethod.getReturnType()); @@ -83,6 +97,16 @@ class RagComponentStructureTests { assertEquals(RagDocumentResponse.class, RagDocumentResponse.class.getMethod("fromEntity", RagDocument.class).getReturnType()); } + @Test + void ragDocumentListUrlShouldUseExplicitListAction() throws NoSuchMethodException { + Method documentListMethod = RagDocumentController.class.getMethod("list"); + + PostMapping postMapping = documentListMethod.getAnnotation(PostMapping.class); + + assertNotNull(postMapping); + assertEquals("/list", postMapping.value()[0]); + } + @Test void ragSourceTypesAndDocumentRelationShouldExist() throws NoSuchFieldException { Field storeIdField = RagDocument.class.getDeclaredField("storeId"); @@ -90,6 +114,7 @@ class RagComponentStructureTests { assertEquals("RAG_STORE", RagSystemConstants.RAG_STORE); assertEquals("RAG_DOCUMENT", RagSystemConstants.RAG_DOCUMENT); + assertEquals("RAG", RagSystemConstants.SOURCE_TYPE_RAG); assertEquals(Long.class, storeIdField.getType()); assertEquals(Long.class, attachmentIdField.getType()); assertTrue(RagStore.class.getSimpleName().contains("RagStore")); diff --git a/src/test/java/com/bruce/rag/RagStoreOverviewServiceTests.java b/src/test/java/com/bruce/rag/RagStoreOverviewServiceTests.java new file mode 100644 index 0000000..2f2ceec --- /dev/null +++ b/src/test/java/com/bruce/rag/RagStoreOverviewServiceTests.java @@ -0,0 +1,126 @@ +package com.bruce.rag; + +import com.bruce.common.enums.EnableStatusEnum; +import com.bruce.rag.dto.response.RagDocumentResponse; +import com.bruce.rag.dto.response.RagStoreDocumentOverviewResponse; +import com.bruce.rag.dto.response.RagStoreOverviewResponse; +import com.bruce.rag.entity.RagStore; +import com.bruce.rag.enums.RagIndexStatusEnum; +import com.bruce.rag.enums.RagParseStatusEnum; +import com.bruce.rag.service.IRagDocumentService; +import com.bruce.rag.service.impl.RagStoreServiceImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RagStoreOverviewServiceTests { + + @Spy + @InjectMocks + private RagStoreServiceImpl ragStoreService; + + @Mock + private IRagDocumentService ragDocumentService; + + @Test + void getOverviewShouldAggregateStoreAndDocumentCounts() { + RagStore enabledStore = new RagStore(); + enabledStore.setId(1L); + enabledStore.setStatus(EnableStatusEnum.ENABLED.getLabel()); + RagStore disabledStore = new RagStore(); + disabledStore.setId(2L); + disabledStore.setStatus("停用"); + + when(ragDocumentService.listResponses()).thenReturn(List.of( + createDocumentResponse("11", "1", true, RagParseStatusEnum.UPLOADED.name(), RagIndexStatusEnum.PENDING.name(), new Date()), + createDocumentResponse("22", "2", false, RagParseStatusEnum.PARSED.name(), RagIndexStatusEnum.INDEXED.name(), new Date()) + )); + doReturn(List.of(enabledStore, disabledStore)).when(ragStoreService).list(); + + RagStoreOverviewResponse response = ragStoreService.getOverview(); + + assertEquals(2, response.getTotalStores()); + assertEquals(2, response.getTotalDocuments()); + assertNull(response.getTotalChunks()); + assertEquals(1, response.getRetrievableStores()); + } + + @Test + void getDocumentOverviewShouldAggregateCurrentStoreDocumentMetrics() { + RagStore store = new RagStore(); + store.setId(1L); + store.setStoreName("产品制度库"); + doReturn(store).when(ragStoreService).getById(1L); + when(ragDocumentService.query(org.mockito.ArgumentMatchers.any())).thenReturn(List.of( + createDocumentResponse("11", "1", true, RagParseStatusEnum.UPLOADED.name(), RagIndexStatusEnum.PENDING.name(), new Date(1747816496000L)), + createDocumentResponse("12", "1", true, RagParseStatusEnum.PARSED.name(), RagIndexStatusEnum.INDEXED.name(), new Date(1747820096000L)), + createDocumentResponse("13", "1", false, RagParseStatusEnum.FAILED.name(), RagIndexStatusEnum.FAILED.name(), new Date(1747812896000L)) + )); + + RagStoreDocumentOverviewResponse response = ragStoreService.getDocumentOverview(1L); + + assertEquals(1L, response.getStoreId()); + assertEquals("产品制度库", response.getStoreName()); + assertEquals(3, response.getDocumentCount()); + assertEquals(2, response.getEnabledDocumentCount()); + assertEquals(1, response.getParsedDocumentCount()); + assertEquals(1, response.getIndexedDocumentCount()); + assertEquals(new Date(1747820096000L), response.getLastUploadTime()); + } + + @Test + void getDocumentOverviewShouldQueryDocumentsByStoreIdOnly() { + RagStore store = new RagStore(); + store.setId(1L); + store.setStoreName("产品制度库"); + doReturn(store).when(ragStoreService).getById(1L); + when(ragDocumentService.query(org.mockito.ArgumentMatchers.any())).thenReturn(List.of()); + + ragStoreService.getDocumentOverview(1L); + + org.mockito.ArgumentCaptor captor = + org.mockito.ArgumentCaptor.forClass(com.bruce.rag.dto.request.RagDocumentQueryRequest.class); + org.mockito.Mockito.verify(ragDocumentService).query(captor.capture()); + assertEquals(1L, captor.getValue().getStoreId()); + assertNull(captor.getValue().getParseStatus()); + assertNull(captor.getValue().getIndexStatus()); + } + + @Test + void getDocumentOverviewShouldRejectUnknownStore() { + doReturn(null).when(ragStoreService).getById(999L); + + assertThrows(IllegalArgumentException.class, () -> ragStoreService.getDocumentOverview(999L)); + } + + private RagDocumentResponse createDocumentResponse( + String id, + String storeId, + boolean enabled, + String parseStatus, + String indexStatus, + Date createTime + ) { + RagDocumentResponse response = new RagDocumentResponse(); + response.setId(Long.valueOf(id)); + response.setStoreId(Long.valueOf(storeId)); + response.setEnabled(enabled); + response.setParseStatus(parseStatus); + response.setIndexStatus(indexStatus); + response.setCreateTime(createTime); + return response; + } +}